From c9eebafb2dd2b2db366a4cd1cbd336113b03dddf Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Jan 2021 16:25:14 +0100 Subject: [PATCH 001/104] Add core structure for ODT class --- nw/core/__init__.py | 2 ++ nw/core/toodt.py | 55 ++++++++++++++++++++++++++++++ nw/gui/projdetails.py | 2 +- tests/test_core/test_core_toodt.py | 35 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 nw/core/toodt.py create mode 100644 tests/test_core/test_core_toodt.py diff --git a/nw/core/__init__.py b/nw/core/__init__.py index f5140ebc..fc90379b 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -5,6 +5,7 @@ from nw.core.index import NWIndex from nw.core.project import NWProject from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple from nw.core.tohtml import ToHtml +from nw.core.toodt import ToOdt from nw.core.tools import countWords, numberToRoman, numberToWord __all__ = [ @@ -18,4 +19,5 @@ __all__ = [ "NWSpellEnchant", "NWSpellSimple", "ToHtml", + "ToOdt", ] diff --git a/nw/core/toodt.py b/nw/core/toodt.py new file mode 100644 index 00000000..5bad6615 --- /dev/null +++ b/nw/core/toodt.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +novelWriter – ODT Text Converter +================================ +Extends the Tokenizer class to generate ODT and FODT files + +File History: +Created: 2021-01-26 [1.1rc1] + +This file is a part of novelWriter +Copyright 2018–2021, 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 . +""" + +import logging + +from nw.core.tokenizer import Tokenizer + +logger = logging.getLogger(__name__) + +class ToOdt(Tokenizer): + + def __init__(self, theProject, theParent): + Tokenizer.__init__(self, theProject, theParent) + + return + + ## + # Class Methods + ## + + def doConvert(self): + """Convert the list of text tokens into a HTML document saved + to theResult. + """ + self.theResult = "" + + for tType, tLine, tText, tFormat, tStyle in self.theTokens: + continue + + return + +# END Class ToOdt diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 0fabaae9..d37cebee 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -5,7 +5,7 @@ novelWriter – GUI Project Details Class holding the project details dialog File History: -Created: 2021-01-03 [1.0a0] +Created: 2021-01-03 [1.1a0] This file is a part of novelWriter Copyright 2018–2021, Veronica Berglyd Olsen diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py new file mode 100644 index 00000000..c9fe5b10 --- /dev/null +++ b/tests/test_core/test_core_toodt.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +novelWriter – ToOdt Class Tester +================================= + +This file is a part of novelWriter +Copyright 2018–2021, 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 . +""" + +import pytest + +from nw.core import NWProject, NWIndex, ToOdt + +@pytest.mark.core +def testCoreToOdt_Convert(dummyGUI): + """Test the converter of the ToHtml class. + """ + theProject = NWProject(dummyGUI) + dummyGUI.theIndex = NWIndex(theProject, dummyGUI) + theDoc = ToOdt(theProject, dummyGUI) + +# END Test testCoreToOdt_Convert From e3c37f003be2eb6466a1a192e121de415ade20b6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Jan 2021 20:22:32 +0100 Subject: [PATCH 002/104] Add main ODT style XML --- nw/core/toodt.py | 303 ++++++++++++++++++++++++++++- tests/test_core/test_core_toodt.py | 18 +- 2 files changed, 319 insertions(+), 2 deletions(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 5bad6615..09fbb91f 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -24,23 +24,87 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import nw import logging +import os + +from lxml import etree from nw.core.tokenizer import Tokenizer logger = logging.getLogger(__name__) +XML_NS = { + "office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0", + "style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0", + "text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0", + "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", + "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0", +} + class ToOdt(Tokenizer): def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) + self.mainConf = nw.CONFIG + + self._xRoot = None + self._xStyl = None + self._xText = None + + self._dLanguage = "en" + self._dCountry = "GB" + self._dFontFace = "Liberation Serif" + self._dFontSize = 12 + + return + + ## + # Setters + ## + + def setLanguage(self, theLang): + """Set language for the document. + """ + if theLang is None: + return False + + langBits = theLang.split("_") + self._dLanguage = langBits[0] + if len(langBits) > 1: + self._dCountry = langBits[1] + + return True + + def setFont(self, fontFace, fontSize): + """Set font and font size. + """ + self._dFontFace = fontFace + self._dFontSize = fontSize return ## # Class Methods ## + def initDocument(self): + """Initialises a new open document XML tree. + """ + rAttr = { + _mkTag("office", "version") : "1.3", + _mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text", + } + self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS) + self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles")) + self._xText = etree.SubElement(self._xRoot, _mkTag("office", "text")) + + # Add Styles + self._styleParagraph() + self._styleHeaders() + + return + def doConvert(self): """Convert the list of text tokens into a HTML document saved to theResult. @@ -48,8 +112,245 @@ class ToOdt(Tokenizer): self.theResult = "" for tType, tLine, tText, tFormat, tStyle in self.theTokens: - continue + + # Process Text Type + if tType == self.T_EMPTY: + continue + + elif tType == self.T_TITLE: + continue + + return + + def closeDocument(self): + """Return the serialised XML document + """ + self.theResult = etree.tostring( + self._xRoot, + pretty_print = True, + encoding = "utf-8", + xml_declaration = True + ) + + cacheFile = os.path.join(os.path.expanduser("~"), "Temp", "odtGen.fodt") + with open(cacheFile, mode="wb") as outFile: + outFile.write(self.theResult) + + return + + ## + # Style Elements + ## + + def _styleParagraph(self): + """Set the paragraph styles. + """ + # Add Default Paragraph Style + # =========================== + + theAttr = {} + theAttr[_mkTag("style", "family")] = "paragraph" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("style", "line-break")] = "strict" + theAttr[_mkTag("style", "tab-stop-distance")] = "1.251cm" + theAttr[_mkTag("style", "writing-mode")] = "page" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("style", "font-name")] = self._dFontFace + theAttr[_mkTag("fo", "font-size")] = "%dpt" % self._dFontSize + theAttr[_mkTag("fo", "language")] = self._dLanguage + theAttr[_mkTag("fo", "country")] = self._dCountry + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + + # Add Paragraph Style + # =================== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Standard" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "class")] = "text" + etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + # Add Text Body Style + # =================== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Text_body" + theAttr[_mkTag("style", "display-name")] = "Text Body" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Standard" + theAttr[_mkTag("style", "class")] = "text" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "0cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.247cm" + theAttr[_mkTag("style", "contextual-spacing")] = "false" + theAttr[_mkTag("fo", "line-height")] = "115%" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + return + + def _styleHeaders(self): + """Set the header styles. + """ + # Add Default Heading Style + # ========================= + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Heading" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Standard" + theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "class")] = "text" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "0.423cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("style", "contextual-spacing")] = "false" + theAttr[_mkTag("fo", "keep-with-next")] = "always" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("style", "font-name")] = self._dFontFace + theAttr[_mkTag("fo", "font-family")] = "'%s'" % self._dFontFace + theAttr[_mkTag("style", "font-pitch")] = "variable" + theAttr[_mkTag("fo", "font-size")] = "14pt" + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + + # Add Title Style + # =============== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Title" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Heading" + theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "class")] = "chapter" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "text-align")] = "center" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "font-size")] = "28pt" + theAttr[_mkTag("fo", "font-weight")] = "bold" + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + + # Add Heading 1 Style + # =================== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Heading_1" + theAttr[_mkTag("style", "display-name")] = "Heading 1" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Heading" + theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "default-outline-level")] = "1" + theAttr[_mkTag("style", "class")] = "text" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "0.423cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("style", "contextual-spacing")] = "false" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "font-size")] = "130%" + theAttr[_mkTag("fo", "font-weight")] = "bold" + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + + # Add Heading 2 Style + # =================== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Heading_2" + theAttr[_mkTag("style", "display-name")] = "Heading 2" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Heading" + theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "default-outline-level")] = "2" + theAttr[_mkTag("style", "class")] = "text" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "0.353cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("style", "contextual-spacing")] = "false" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "font-size")] = "120%" + theAttr[_mkTag("fo", "font-weight")] = "bold" + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + + # Add Heading 3 Style + # =================== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Heading_3" + theAttr[_mkTag("style", "display-name")] = "Heading 3" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Heading" + theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "default-outline-level")] = "3" + theAttr[_mkTag("style", "class")] = "text" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "0.247cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("style", "contextual-spacing")] = "false" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "font-size")] = "110%" + theAttr[_mkTag("fo", "font-weight")] = "bold" + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + + # Add Heading 4 Style + # =================== + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Heading_4" + theAttr[_mkTag("style", "display-name")] = "Heading 4" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Heading" + theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "default-outline-level")] = "4" + theAttr[_mkTag("style", "class")] = "text" + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "0.247cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("style", "contextual-spacing")] = "false" + etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "font-size")] = "100%" + theAttr[_mkTag("fo", "font-weight")] = "bold" + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) return # END Class ToOdt + +# =========================================================================== # +# Local Functions +# =========================================================================== # + +def _mkTag(nsName, tagName): + """Assemble namespace and tag name. + """ + theNS = XML_NS.get(nsName, "") + if theNS: + return "{%s}%s" % (theNS, tagName) + logger.warning("Missing xml namespace '%s'" % nsName) + return tagName diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index c9fe5b10..6159929a 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -20,16 +20,32 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import nw import pytest from nw.core import NWProject, NWIndex, ToOdt @pytest.mark.core -def testCoreToOdt_Convert(dummyGUI): +def testCoreToOdt_Convert(tmpConf, dummyGUI): """Test the converter of the ToHtml class. """ + nw.CONFIG = tmpConf + theProject = NWProject(dummyGUI) dummyGUI.theIndex = NWIndex(theProject, dummyGUI) theDoc = ToOdt(theProject, dummyGUI) + # Export Mode + # =========== + + theDoc.isNovel = True + + # Header 1 + theDoc.theText = "# Title\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.theResult == "" + # END Test testCoreToOdt_Convert From da2e40db4dca39e4db45c6f16bae7abeb86e431b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 01:12:09 +0100 Subject: [PATCH 003/104] Add ODT auto-style logic --- nw/core/toodt.py | 478 +++++++++++++++++++++-------- tests/test_core/test_core_toodt.py | 35 ++- 2 files changed, 383 insertions(+), 130 deletions(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 09fbb91f..c89c065d 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -29,6 +29,7 @@ import logging import os from lxml import etree +from hashlib import sha256 from nw.core.tokenizer import Tokenizer @@ -39,7 +40,6 @@ XML_NS = { "style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0", "text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0", "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", - "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0", } class ToOdt(Tokenizer): @@ -51,7 +51,13 @@ class ToOdt(Tokenizer): self._xRoot = None self._xStyl = None + self._xBody = None self._xText = None + self._xAuto = None + + self._mainPara = {} + self._autoPara = {} + self._autoText = {} self._dLanguage = "en" self._dCountry = "GB" @@ -97,11 +103,13 @@ class ToOdt(Tokenizer): } self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS) self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles")) - self._xText = etree.SubElement(self._xRoot, _mkTag("office", "text")) + self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles")) + self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body")) + self._xText = etree.SubElement(self._xBody, _mkTag("office", "text")) # Add Styles - self._styleParagraph() - self._styleHeaders() + self._defaultStyles() + self._useableStyles() return @@ -113,18 +121,60 @@ class ToOdt(Tokenizer): for tType, tLine, tText, tFormat, tStyle in self.theTokens: + # Styles + oStyle = ODTParagraphStyle() + if tStyle is not None: + if tStyle & self.A_LEFT: + oStyle.setTextAlign("left") + if tStyle & self.A_RIGHT: + oStyle.setTextAlign("right") + if tStyle & self.A_CENTRE: + oStyle.setTextAlign("center") + if tStyle & self.A_JUSTIFY: + oStyle.setTextAlign("justify") + if tStyle & self.A_PBB: + oStyle.setBreakBefore("page") + if tStyle & self.A_PBA: + oStyle.setBreakAfter("page") + # Process Text Type if tType == self.T_EMPTY: continue elif tType == self.T_TITLE: - continue + tHead = tText.replace(r"\\", "
") + self._addTextPar("Title", oStyle, tHead) + + elif tType == self.T_HEAD1: + tHead = tText.replace(r"\\", "
") + self._addTextPar("Heading_1", oStyle, tHead, oLevel="1") + + elif tType == self.T_HEAD2: + tHead = tText.replace(r"\\", "
") + self._addTextPar("Heading_2", oStyle, tHead, oLevel="2") + + elif tType == self.T_HEAD3: + tHead = tText.replace(r"\\", "
") + self._addTextPar("Heading_3", oStyle, tHead, oLevel="3") + + elif tType == self.T_HEAD4: + tHead = tText.replace(r"\\", "
") + self._addTextPar("Heading_4", oStyle, tHead, oLevel="4") + + elif tType == self.T_SEP: + self._addTextPar("Text_Body", oStyle, tText) + + elif tType == self.T_SKIP: + self._addTextPar("Text_Body", oStyle, "") return def closeDocument(self): """Return the serialised XML document """ + for styleName, styleObj in self._autoPara.values(): + styleObj.packXML(self._xAuto, styleName) + self.theResult = etree.tostring( self._xRoot, pretty_print = True, @@ -138,15 +188,51 @@ class ToOdt(Tokenizer): return + ## + # Internal Functions + ## + + def _addTextPar(self, styleName, oStyle, theText, oLevel=None): + """Add a text paragraph to the text XML element. + """ + tAttr = {} + tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle) + if oLevel is not None: + tAttr[_mkTag("text", "outline-level")] = oLevel + xElem = etree.SubElement(self._xText, _mkTag("text", "p"), attrib=tAttr) + xElem.text = theText + return + + def _paraStyle(self, parName, oStyle): + """Return a name for a style object. + """ + refStyle = self._mainPara.get(parName, None) + if refStyle is None: + logger.error("Unknown paragraph style '%s'" % parName) + return "Standard" + + if not refStyle.checkNew(oStyle): + return parName + + oStyle.setParentStyleName(parName) + theID = oStyle.getID() + if theID in self._autoPara: + return self._autoPara[theID][0] + + newName = "P%d" % (len(self._autoPara) + 1) + self._autoPara[theID] = (newName, oStyle) + + return newName + ## # Style Elements ## - def _styleParagraph(self): - """Set the paragraph styles. + def _defaultStyles(self): + """Set the default styles. """ - # Add Default Paragraph Style - # =========================== + # Add Paragraph Family Style + # ========================== theAttr = {} theAttr[_mkTag("style", "family")] = "paragraph" @@ -165,8 +251,8 @@ class ToOdt(Tokenizer): theAttr[_mkTag("fo", "country")] = self._dCountry etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) - # Add Paragraph Style - # =================== + # Add Standard Paragraph Style + # ============================ theAttr = {} theAttr[_mkTag("style", "name")] = "Standard" @@ -174,29 +260,6 @@ class ToOdt(Tokenizer): theAttr[_mkTag("style", "class")] = "text" etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) - # Add Text Body Style - # =================== - - theAttr = {} - theAttr[_mkTag("style", "name")] = "Text_body" - theAttr[_mkTag("style", "display-name")] = "Text Body" - theAttr[_mkTag("style", "family")] = "paragraph" - theAttr[_mkTag("style", "parent-style-name")] = "Standard" - theAttr[_mkTag("style", "class")] = "text" - xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) - - theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.247cm" - theAttr[_mkTag("style", "contextual-spacing")] = "false" - theAttr[_mkTag("fo", "line-height")] = "115%" - etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) - - return - - def _styleHeaders(self): - """Set the header styles. - """ # Add Default Heading Style # ========================= @@ -204,14 +267,13 @@ class ToOdt(Tokenizer): theAttr[_mkTag("style", "name")] = "Heading" theAttr[_mkTag("style", "family")] = "paragraph" theAttr[_mkTag("style", "parent-style-name")] = "Standard" - theAttr[_mkTag("style", "next-style-name")] = "Text_body" + theAttr[_mkTag("style", "next-style-name")] = "Text_Body" theAttr[_mkTag("style", "class")] = "text" xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) theAttr = {} theAttr[_mkTag("fo", "margin-top")] = "0.423cm" theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" - theAttr[_mkTag("style", "contextual-spacing")] = "false" theAttr[_mkTag("fo", "keep-with-next")] = "always" etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) @@ -222,129 +284,287 @@ class ToOdt(Tokenizer): theAttr[_mkTag("fo", "font-size")] = "14pt" etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + return + + def _useableStyles(self): + """Set the usable styles. + """ + # Add Text Body Style + # =================== + + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Text Body") + oStyle.setParentStyleName("Standard") + oStyle.setClass("text") + oStyle.setMarginTop("0cm") + oStyle.setMarginBottom("0.247cm") + oStyle.setLineHeight("115%") + oStyle.packXML(self._xStyl, "Text_Body") + + self._mainPara["Text_Body"] = oStyle + # Add Title Style # =============== - theAttr = {} - theAttr[_mkTag("style", "name")] = "Title" - theAttr[_mkTag("style", "family")] = "paragraph" - theAttr[_mkTag("style", "parent-style-name")] = "Heading" - theAttr[_mkTag("style", "next-style-name")] = "Text_body" - theAttr[_mkTag("style", "class")] = "chapter" - xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Title") + oStyle.setParentStyleName("Heading") + oStyle.setNextStyleName("Text_Body") + oStyle.setClass("chapter") + oStyle.setTextAlign("center") + oStyle.setFontSize("28pt") + oStyle.setFontWeight("bold") + oStyle.packXML(self._xStyl, "Title") - theAttr = {} - theAttr[_mkTag("fo", "text-align")] = "center" - etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) - - theAttr = {} - theAttr[_mkTag("fo", "font-size")] = "28pt" - theAttr[_mkTag("fo", "font-weight")] = "bold" - etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + self._mainPara["Title"] = oStyle # Add Heading 1 Style # =================== - theAttr = {} - theAttr[_mkTag("style", "name")] = "Heading_1" - theAttr[_mkTag("style", "display-name")] = "Heading 1" - theAttr[_mkTag("style", "family")] = "paragraph" - theAttr[_mkTag("style", "parent-style-name")] = "Heading" - theAttr[_mkTag("style", "next-style-name")] = "Text_body" - theAttr[_mkTag("style", "default-outline-level")] = "1" - theAttr[_mkTag("style", "class")] = "text" - xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Heading 1") + oStyle.setParentStyleName("Heading") + oStyle.setNextStyleName("Text_Body") + oStyle.setOutlineLevel("1") + oStyle.setClass("text") + oStyle.setMarginTop("0.423cm") + oStyle.setMarginBottom("0.212cm") + oStyle.setFontSize("200%") + oStyle.setFontWeight("bold") + oStyle.packXML(self._xStyl, "Heading_1") - theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0.423cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" - theAttr[_mkTag("style", "contextual-spacing")] = "false" - etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) - - theAttr = {} - theAttr[_mkTag("fo", "font-size")] = "130%" - theAttr[_mkTag("fo", "font-weight")] = "bold" - etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + self._mainPara["Heading_1"] = oStyle # Add Heading 2 Style # =================== - theAttr = {} - theAttr[_mkTag("style", "name")] = "Heading_2" - theAttr[_mkTag("style", "display-name")] = "Heading 2" - theAttr[_mkTag("style", "family")] = "paragraph" - theAttr[_mkTag("style", "parent-style-name")] = "Heading" - theAttr[_mkTag("style", "next-style-name")] = "Text_body" - theAttr[_mkTag("style", "default-outline-level")] = "2" - theAttr[_mkTag("style", "class")] = "text" - xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Heading 2") + oStyle.setParentStyleName("Heading") + oStyle.setNextStyleName("Text_Body") + oStyle.setOutlineLevel("2") + oStyle.setClass("text") + oStyle.setMarginTop("0.353cm") + oStyle.setMarginBottom("0.212cm") + oStyle.setFontSize("140%") + oStyle.setFontWeight("bold") + oStyle.packXML(self._xStyl, "Heading_2") - theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0.353cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" - theAttr[_mkTag("style", "contextual-spacing")] = "false" - etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) - - theAttr = {} - theAttr[_mkTag("fo", "font-size")] = "120%" - theAttr[_mkTag("fo", "font-weight")] = "bold" - etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + self._mainPara["Heading_2"] = oStyle # Add Heading 3 Style # =================== - theAttr = {} - theAttr[_mkTag("style", "name")] = "Heading_3" - theAttr[_mkTag("style", "display-name")] = "Heading 3" - theAttr[_mkTag("style", "family")] = "paragraph" - theAttr[_mkTag("style", "parent-style-name")] = "Heading" - theAttr[_mkTag("style", "next-style-name")] = "Text_body" - theAttr[_mkTag("style", "default-outline-level")] = "3" - theAttr[_mkTag("style", "class")] = "text" - xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Heading 3") + oStyle.setParentStyleName("Heading") + oStyle.setNextStyleName("Text_Body") + oStyle.setOutlineLevel("3") + oStyle.setClass("text") + oStyle.setMarginTop("0.247cm") + oStyle.setMarginBottom("0.212cm") + oStyle.setFontSize("125%") + oStyle.setFontWeight("bold") + oStyle.packXML(self._xStyl, "Heading_3") - theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0.247cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" - theAttr[_mkTag("style", "contextual-spacing")] = "false" - etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) - - theAttr = {} - theAttr[_mkTag("fo", "font-size")] = "110%" - theAttr[_mkTag("fo", "font-weight")] = "bold" - etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + self._mainPara["Heading_3"] = oStyle # Add Heading 4 Style # =================== - theAttr = {} - theAttr[_mkTag("style", "name")] = "Heading_4" - theAttr[_mkTag("style", "display-name")] = "Heading 4" - theAttr[_mkTag("style", "family")] = "paragraph" - theAttr[_mkTag("style", "parent-style-name")] = "Heading" - theAttr[_mkTag("style", "next-style-name")] = "Text_body" - theAttr[_mkTag("style", "default-outline-level")] = "4" - theAttr[_mkTag("style", "class")] = "text" - xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Heading 4") + oStyle.setParentStyleName("Heading") + oStyle.setNextStyleName("Text_Body") + oStyle.setOutlineLevel("4") + oStyle.setClass("text") + oStyle.setMarginTop("0.247cm") + oStyle.setMarginBottom("0.212cm") + oStyle.setFontSize("110%") + oStyle.setFontWeight("bold") + oStyle.packXML(self._xStyl, "Heading_4") - theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0.247cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" - theAttr[_mkTag("style", "contextual-spacing")] = "false" - etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) - - theAttr = {} - theAttr[_mkTag("fo", "font-size")] = "100%" - theAttr[_mkTag("fo", "font-weight")] = "bold" - etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + self._mainPara["Heading_4"] = oStyle return # END Class ToOdt -# =========================================================================== # +# =============================================================================================== # +# Auto-Style Classes +# =============================================================================================== # + +class ODTParagraphStyle(): + + VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"] + VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"] + VALID_LEVEL = ["1", "2", "3", "4"] + VALID_CLASS = ["text", "chapter"] + VALID_WEIGHT = ["normal", "inherit", "bold"] + + def __init__(self): + + # Attributes + self._mAttr = { + "display-name": ["style", None], + "parent-style-name": ["style", None], + "next-style-name": ["style", None], + "default-outline-level": ["style", None], + "class": ["style", None], + } + + # Paragraph Attributes + self._pAttr = { + "margin-top": ["fo", None], + "margin-bottom": ["fo", None], + "line-height": ["fo", None], + "text-align": ["fo", None], + "break-before": ["fo", None], + "break-after": ["fo", None], + } + + # text Attributes + self._tAttr = { + "font-size": ["fo", None], + "font-weight": ["fo", None], + } + + return + + ## + # Attribute Setters + ## + + def setDisplayName(self, theValue): + self._mAttr["display-name"][1] = str(theValue) + return + + def setParentStyleName(self, theValue): + self._mAttr["parent-style-name"][1] = str(theValue) + return + + def setNextStyleName(self, theValue): + self._mAttr["next-style-name"][1] = str(theValue) + return + + def setOutlineLevel(self, theValue): + if theValue in self.VALID_LEVEL: + self._mAttr["default-outline-level"][1] = str(theValue) + return + + def setClass(self, theValue): + if theValue in self.VALID_CLASS: + self._mAttr["class"][1] = str(theValue) + return + + ## + # Paragraph Setters + ## + + def setMarginTop(self, theValue): + self._pAttr["margin-top"][1] = str(theValue) + return + + def setMarginBottom(self, theValue): + self._pAttr["margin-bottom"][1] = str(theValue) + return + + def setLineHeight(self, theValue): + self._pAttr["line-height"][1] = str(theValue) + return + + def setTextAlign(self, theValue): + if theValue in self.VALID_ALIGN: + self._pAttr["text-align"][1] = str(theValue) + return + + def setBreakBefore(self, theValue): + if theValue in self.VALID_BREAK: + self._pAttr["break-before"][1] = str(theValue) + return + + def setBreakAfter(self, theValue): + if theValue in self.VALID_BREAK: + self._pAttr["break-after"][1] = str(theValue) + return + + ## + # Text Setters + ## + + def setFontSize(self, theValue): + self._tAttr["font-size"][1] = str(theValue) + return + + def setFontWeight(self, theValue): + if theValue in self.VALID_WEIGHT: + self._tAttr["font-weight"][1] = str(theValue) + return + + ## + # Methods + ## + + def checkNew(self, refStyle): + """Check if there are new settings in refStyle that differ from + those in the current object. + """ + for aName, (aNm, aVal) in refStyle._mAttr.items(): + if aVal is not None and aVal != self._mAttr[aName][1]: + return True + for aName, (aNm, aVal) in refStyle._pAttr.items(): + if aVal is not None and aVal != self._pAttr[aName][1]: + return True + for aName, (aNm, aVal) in refStyle._tAttr.items(): + if aVal is not None and aVal != self._tAttr[aName][1]: + return True + return False + + def getID(self): + """Generate a unique ID from the settings. + """ + theString = ( + f"Paragraph:Main:{str(self._mAttr)}:" + f"Paragraph:Para:{str(self._pAttr)}:" + f"Paragraph:Text:{str(self._tAttr)}:" + ) + return sha256(theString.encode()).hexdigest() + + def packXML(self, xParent, xName): + """Pack the content into an xml element. + """ + theAttr = {} + theAttr[_mkTag("style", "name")] = xName + theAttr[_mkTag("style", "family")] = "paragraph" + for aName, (aNm, aVal) in self._mAttr.items(): + if aVal is not None: + theAttr[_mkTag(aNm, aName)] = aVal + + xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + for aName, (aNm, aVal) in self._pAttr.items(): + if aVal is not None: + theAttr[_mkTag(aNm, aName)] = aVal + + if theAttr: + etree.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr) + + theAttr = {} + for aName, (aNm, aVal) in self._tAttr.items(): + if aVal is not None: + theAttr[_mkTag(aNm, aName)] = aVal + + if theAttr: + etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr) + + return + +# END Class ODTParagraphStyle + +# =============================================================================================== # # Local Functions -# =========================================================================== # +# =============================================================================================== # def _mkTag(nsName, tagName): """Assemble namespace and tag name. diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 6159929a..c0e8c797 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -23,8 +23,25 @@ along with this program. If not, see . import nw import pytest +from lxml import etree + from nw.core import NWProject, NWIndex, ToOdt +XML_NS = [ + ' 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"', + ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"', +] + +def xmlToText(xElem): + """Get the text content of an XML element. + """ + rTxt = etree.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() + for nSpace in XML_NS: + rTxt = rTxt.replace(nSpace, "") + return rTxt + @pytest.mark.core def testCoreToOdt_Convert(tmpConf, dummyGUI): """Test the converter of the ToHtml class. @@ -46,6 +63,22 @@ def testCoreToOdt_Convert(tmpConf, dummyGUI): theDoc.initDocument() theDoc.doConvert() theDoc.closeDocument() - assert theDoc.theResult == "" + assert xmlToText(theDoc._xText) == ( + '' + 'Title' + '' + ) + + # Header 1 + theDoc.theText = "## Chapter Title\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert xmlToText(theDoc._xText) == ( + '' + 'Chapter Title' + '' + ) # END Test testCoreToOdt_Convert From 1683567dda04e67ed26743a096e194062f905db9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 01:12:34 +0100 Subject: [PATCH 004/104] Restructure Build Novel Project tool to allow ODT save --- nw/gui/build.py | 221 ++++++++++++++++++++++++++++-------------------- 1 file changed, 130 insertions(+), 91 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index c24edb3e..af7d71d1 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -46,7 +46,7 @@ from PyQt5.QtWidgets import ( from nw.common import fuzzyTime, makeFileNameSafe from nw.gui.custom import QSwitch -from nw.core import ToHtml +from nw.core import ToHtml, ToOdt from nw.constants import ( nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass ) @@ -56,13 +56,14 @@ logger = logging.getLogger(__name__) class GuiBuildNovel(QDialog): FMT_ODT = 1 - FMT_PDF = 2 - FMT_HTM = 3 - FMT_MD = 4 - FMT_NWD = 5 - FMT_TXT = 6 - FMT_JSON_H = 7 - FMT_JSON_M = 8 + FMT_FODT = 2 + FMT_PDF = 3 + FMT_HTM = 4 + FMT_MD = 5 + FMT_NWD = 6 + FMT_TXT = 7 + FMT_JSON_H = 8 + FMT_JSON_M = 9 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -386,6 +387,10 @@ class GuiBuildNovel(QDialog): self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT)) self.saveMenu.addAction(self.saveODT) + self.saveFODT = QAction("Flat Open Document (.fodt)", self) + self.saveFODT.triggered.connect(lambda: self._saveDocument(self.FMT_FODT)) + self.saveMenu.addAction(self.saveFODT) + self.savePDF = QAction("Portable Document Format (.pdf)", self) self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) self.saveMenu.addAction(self.savePDF) @@ -533,49 +538,18 @@ class GuiBuildNovel(QDialog): return True ## - # Slots + # Slots and Related ## def _buildPreview(self): """Build a preview of the project in the document viewer. """ # Get Settings - fmtTitle = self.fmtTitle.text().strip() - fmtChapter = self.fmtChapter.text().strip() - fmtUnnumbered = self.fmtUnnumbered.text().strip() - fmtScene = self.fmtScene.text().strip() - fmtSection = self.fmtSection.text().strip() - justifyText = self.justifyText.isChecked() - noStyling = self.noStyling.isChecked() - textFont = self.textFont.text() - textSize = self.textSize.value() - incSynopsis = self.includeSynopsis.isChecked() - incComments = self.includeComments.isChecked() - incKeywords = self.includeKeywords.isChecked() - novelFiles = self.novelFiles.isChecked() - noteFiles = self.noteFiles.isChecked() - ignoreFlag = self.ignoreFlag.isChecked() - includeBody = self.includeBody.isChecked() - replaceTabs = self.replaceTabs.isChecked() - - makeHtml = ToHtml(self.theProject, self.theParent) - makeHtml.setTitleFormat(fmtTitle) - makeHtml.setChapterFormat(fmtChapter) - makeHtml.setUnNumberedFormat(fmtUnnumbered) - makeHtml.setSceneFormat(fmtScene, fmtScene == "") - makeHtml.setSectionFormat(fmtSection, fmtSection == "") - makeHtml.setBodyText(includeBody) - makeHtml.setSynopsis(incSynopsis) - makeHtml.setComments(incComments) - makeHtml.setKeywords(incKeywords) - makeHtml.setJustify(justifyText) - makeHtml.setStyles(not noStyling) - - # Make sure the tree order is correct - self.theParent.treeView.flushTreeOrder() - - self.buildProgress.setMaximum(len(self.theProject.projTree)) - self.buildProgress.setValue(0) + justifyText = self.justifyText.isChecked() + noStyling = self.noStyling.isChecked() + textFont = self.textFont.text() + textSize = self.textSize.value() + replaceTabs = self.replaceTabs.isChecked() tStart = int(time()) @@ -583,52 +557,8 @@ class GuiBuildNovel(QDialog): self.htmlStyle = [] self.nwdText = [] - htmlSize = 0 - - for nItt, tItem in enumerate(self.theProject.projTree): - - noteRoot = noteFiles - noteRoot &= tItem.itemType == nwItemType.ROOT - noteRoot &= tItem.itemClass != nwItemClass.NOVEL - noteRoot &= tItem.itemClass != nwItemClass.ARCHIVE - - try: - if noteRoot: - # Add headers for root folders of notes - makeHtml.addRootHeading(tItem.itemHandle) - makeHtml.doConvert() - self.htmlText.append(makeHtml.getResult()) - self.nwdText.append(makeHtml.getFilteredMarkdown()) - htmlSize += makeHtml.getResultSize() - - elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): - makeHtml.setText(tItem.itemHandle) - makeHtml.doAutoReplace() - makeHtml.tokenizeText() - makeHtml.doHeaders() - makeHtml.doConvert() - makeHtml.doPostProcessing() - self.htmlText.append(makeHtml.getResult()) - self.nwdText.append(makeHtml.getFilteredMarkdown()) - htmlSize += makeHtml.getResultSize() - - except Exception as e: - logger.error("Failed to generate html of document '%s'" % tItem.itemHandle) - logger.error(str(e)) - self.docView.setText(( - "Failed to generate preview. " - "Document with title '%s' could not be parsed." - ) % tItem.itemName) - return False - - # Update progress bar, also for skipped items - self.buildProgress.setValue(nItt+1) - - if makeHtml.errData: - self.theParent.makeAlert(( - "There were problems when building the project:" - "
- %s" - ) % "
- ".join(makeHtml.errData), nwAlert.ERROR) + makeHtml = ToHtml(self.theProject, self.theParent) + htmlSize = self._doBuild(makeHtml) if replaceTabs: htmlText = [] @@ -668,6 +598,103 @@ class GuiBuildNovel(QDialog): return + def _doBuild(self, bldObj): + """Rund the build with a specific build object. + """ + # Get Settings + fmtTitle = self.fmtTitle.text().strip() + fmtChapter = self.fmtChapter.text().strip() + fmtUnnumbered = self.fmtUnnumbered.text().strip() + fmtScene = self.fmtScene.text().strip() + fmtSection = self.fmtSection.text().strip() + justifyText = self.justifyText.isChecked() + noStyling = self.noStyling.isChecked() + incSynopsis = self.includeSynopsis.isChecked() + incComments = self.includeComments.isChecked() + incKeywords = self.includeKeywords.isChecked() + novelFiles = self.novelFiles.isChecked() + noteFiles = self.noteFiles.isChecked() + ignoreFlag = self.ignoreFlag.isChecked() + includeBody = self.includeBody.isChecked() + + isHtml = isinstance(bldObj, ToHtml) + isOdt = isinstance(bldObj, ToOdt) + + bldObj.setTitleFormat(fmtTitle) + bldObj.setChapterFormat(fmtChapter) + bldObj.setUnNumberedFormat(fmtUnnumbered) + bldObj.setSceneFormat(fmtScene, fmtScene == "") + bldObj.setSectionFormat(fmtSection, fmtSection == "") + bldObj.setBodyText(includeBody) + bldObj.setSynopsis(incSynopsis) + bldObj.setComments(incComments) + bldObj.setKeywords(incKeywords) + bldObj.setJustify(justifyText) + + if isHtml: + bldObj.setStyles(not noStyling) + + if isOdt: + bldObj.initDocument() + + # Make sure the tree order is correct + self.theParent.treeView.flushTreeOrder() + + self.buildProgress.setMaximum(len(self.theProject.projTree)) + self.buildProgress.setValue(0) + + accSize = 0 + + for nItt, tItem in enumerate(self.theProject.projTree): + + noteRoot = noteFiles + noteRoot &= tItem.itemType == nwItemType.ROOT + noteRoot &= tItem.itemClass != nwItemClass.NOVEL + noteRoot &= tItem.itemClass != nwItemClass.ARCHIVE + + try: + if noteRoot: + # Add headers for root folders of notes + bldObj.addRootHeading(tItem.itemHandle) + bldObj.doConvert() + self.htmlText.append(bldObj.getResult()) + self.nwdText.append(bldObj.getFilteredMarkdown()) + accSize += bldObj.getResultSize() + + elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): + bldObj.setText(tItem.itemHandle) + bldObj.doAutoReplace() + bldObj.tokenizeText() + bldObj.doHeaders() + bldObj.doConvert() + bldObj.doPostProcessing() + self.htmlText.append(bldObj.getResult()) + self.nwdText.append(bldObj.getFilteredMarkdown()) + accSize += bldObj.getResultSize() + + except Exception as e: + logger.error("Failed to generate html of document '%s'" % tItem.itemHandle) + logger.error(str(e)) + self.docView.setText(( + "Failed to generate preview. " + "Document with title '%s' could not be parsed." + ) % tItem.itemName) + return False + + # Update progress bar, also for skipped items + self.buildProgress.setValue(nItt+1) + + if isOdt: + bldObj.closeDocument() + + if bldObj.errData: + self.theParent.makeAlert(( + "There were problems when building the project:" + "
- %s" + ) % "
- ".join(bldObj.errData), nwAlert.ERROR) + + return accSize + def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag): """This function checks whether a file should be included in the export or not. For standard note and novel files, this is @@ -723,6 +750,11 @@ class GuiBuildNovel(QDialog): textFmt = "Open Document" outTool = "Qt" + elif theFormat == self.FMT_FODT: + fileExt = "fodt" + textFmt = "Flat Open Document" + outTool = "NW2" + elif theFormat == self.FMT_PDF: fileExt = "pdf" textFmt = "PDF" @@ -866,6 +898,13 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) + elif outTool == "NW2": + + if theFormat == self.FMT_FODT: + makeOdt = ToOdt(self.theProject, self.theParent) + self._doBuild(makeOdt) + wSuccess = True + elif outTool == "QtPrint" and theFormat == self.FMT_PDF: try: thePrinter = QPrinter() From cbd222fe309ff84a427a38fac081f61afdd31764 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 02:35:19 +0100 Subject: [PATCH 005/104] Add code to save tabs and line breaks to ODT file --- nw/core/toodt.py | 116 +++++++++++++++++++++++++++++++++++++++++------ nw/gui/build.py | 13 ++++-- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index c89c065d..48c2765e 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -42,6 +42,9 @@ XML_NS = { "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", } +X_BR = "{%s}line-break" % XML_NS["text"] +X_TAB = "{%s}tab" % XML_NS["text"] + class ToOdt(Tokenizer): def __init__(self, theProject, theParent): @@ -119,6 +122,9 @@ class ToOdt(Tokenizer): """ self.theResult = "" + thisPar = [] + parStyle = None + hasHardBreak = False for tType, tLine, tText, tFormat, tStyle in self.theTokens: # Styles @@ -139,27 +145,35 @@ class ToOdt(Tokenizer): # Process Text Type if tType == self.T_EMPTY: - continue + if hasHardBreak and parStyle is not None: + if self.doJustify: + parStyle.setTextAlign("left") + if len(thisPar) > 0: + tTemp = "".join(thisPar) + self._addTextPar("Text_Body", parStyle, tTemp.rstrip()) + thisPar = [] + parStyle = None + hasHardBreak = False elif tType == self.T_TITLE: - tHead = tText.replace(r"\\", "
") - self._addTextPar("Title", oStyle, tHead) + tHead = tText.replace(r"\\", "\n") + self._addTextPar("Title", oStyle, tHead, isHead=True) elif tType == self.T_HEAD1: - tHead = tText.replace(r"\\", "
") - self._addTextPar("Heading_1", oStyle, tHead, oLevel="1") + tHead = tText.replace(r"\\", "\n") + self._addTextPar("Heading_1", oStyle, tHead, isHead=True, oLevel="1") elif tType == self.T_HEAD2: - tHead = tText.replace(r"\\", "
") - self._addTextPar("Heading_2", oStyle, tHead, oLevel="2") + tHead = tText.replace(r"\\", "\n") + self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2") elif tType == self.T_HEAD3: - tHead = tText.replace(r"\\", "
") - self._addTextPar("Heading_3", oStyle, tHead, oLevel="3") + tHead = tText.replace(r"\\", "\n") + self._addTextPar("Heading_3", oStyle, tHead, isHead=True, oLevel="3") elif tType == self.T_HEAD4: - tHead = tText.replace(r"\\", "
") - self._addTextPar("Heading_4", oStyle, tHead, oLevel="4") + tHead = tText.replace(r"\\", "\n") + self._addTextPar("Heading_4", oStyle, tHead, isHead=True, oLevel="4") elif tType == self.T_SEP: self._addTextPar("Text_Body", oStyle, tText) @@ -167,6 +181,18 @@ class ToOdt(Tokenizer): elif tType == self.T_SKIP: self._addTextPar("Text_Body", oStyle, "") + elif tType == self.T_TEXT: + tTemp = tText + if parStyle is None: + parStyle = oStyle + # for xPos, xLen, xFmt in reversed(tFormat): + # tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:] + if tText.endswith(" "): + thisPar.append(tTemp.rstrip()+"\n") + hasHardBreak = True + else: + thisPar.append(tTemp.rstrip()+" ") + return def closeDocument(self): @@ -192,15 +218,51 @@ class ToOdt(Tokenizer): # Internal Functions ## - def _addTextPar(self, styleName, oStyle, theText, oLevel=None): + def _addTextPar(self, styleName, oStyle, theText, isHead=False, oLevel=None): """Add a text paragraph to the text XML element. """ tAttr = {} tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle) if oLevel is not None: tAttr[_mkTag("text", "outline-level")] = oLevel - xElem = etree.SubElement(self._xText, _mkTag("text", "p"), attrib=tAttr) - xElem.text = theText + + pTag = "h" if isHead else "p" + xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr) + + if not theText: + return + + if "\t" not in theText and "\n" not in theText: + xElem.text = theText + return + + # Process tabs and line breaks + tTemp = "" + xTail = None + for c in theText: + if c == "\t": + if xTail is None: + xElem.text = tTemp + else: + xTail.tail = tTemp + tTemp = "" + xTail = etree.SubElement(xElem, X_TAB) + elif c == "\n": + if xTail is None: + xElem.text = tTemp + else: + xTail.tail = tTemp + tTemp = "" + xTail = etree.SubElement(xElem, X_BR) + else: + tTemp += c + + if tTemp != "": + if xTail is None: + xElem.text = tTemp + else: + xTail.tail = tTemp + return def _paraStyle(self, parName, oStyle): @@ -299,6 +361,10 @@ class ToOdt(Tokenizer): oStyle.setMarginTop("0cm") oStyle.setMarginBottom("0.247cm") oStyle.setLineHeight("115%") + if self.doJustify: + oStyle.setTextAlign("justify") + else: + oStyle.setTextAlign("left") oStyle.packXML(self._xStyl, "Text_Body") self._mainPara["Text_Body"] = oStyle @@ -501,6 +567,28 @@ class ODTParagraphStyle(): self._tAttr["font-weight"][1] = str(theValue) return + ## + # Getters + ## + + def getAttr(self, attrName): + """Look through the dictionaries for the value, and return it if + we can find it, If not, return None. + """ + retVal = self._mAttr.get(attrName, None) + if retVal is not None: + return retVal + + retVal = self._pAttr.get(attrName, None) + if retVal is not None: + return retVal + + retVal = self._tAttr.get(attrName, None) + if retVal is not None: + return retVal + + return None + ## # Methods ## diff --git a/nw/gui/build.py b/nw/gui/build.py index af7d71d1..4584d44b 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -753,7 +753,7 @@ class GuiBuildNovel(QDialog): elif theFormat == self.FMT_FODT: fileExt = "fodt" textFmt = "Flat Open Document" - outTool = "NW2" + outTool = "NW_ODT" elif theFormat == self.FMT_PDF: fileExt = "pdf" @@ -898,12 +898,19 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif outTool == "NW2": + elif outTool == "NW_ODT": if theFormat == self.FMT_FODT: makeOdt = ToOdt(self.theProject, self.theParent) self._doBuild(makeOdt) - wSuccess = True + try: + with open(savePath, mode="wb") as outFile: + outFile.write(makeOdt.theResult) + + wSuccess = True + + except Exception as e: + errMsg = str(e) elif outTool == "QtPrint" and theFormat == self.FMT_PDF: try: From 8ccd6a8ed3bdec4dd18fdbeeaaa966d73d7d1f9c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 12:38:31 +0100 Subject: [PATCH 006/104] Fix setting font setting for ODT export --- nw/core/tokenizer.py | 32 ++++++++--- nw/core/toodt.py | 130 +++++++++++++++++++++++++++++++------------ nw/gui/build.py | 15 ++++- 3 files changed, 130 insertions(+), 47 deletions(-) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 6fa7a87a..4e0c6290 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -87,11 +87,15 @@ class Tokenizer(): self.theMarkdown = None # The result text in novelWriter markdown # User Settings - self.doBodyText = True # Include body text - self.doSynopsis = False # Also process synopsis comments - self.doComments = False # Also process comments - self.doKeywords = False # Also process keywords like tags and references - self.doJustify = False # Justify text + self.textFont = "Serif" # Output text font + self.textSize = 11 # Output text size + self.textFixed = False # Fixed width text + self.lineHeight = 1.15 # Line height + self.doJustify = False # Justify text + self.doBodyText = True # Include body text + self.doSynopsis = False # Also process synopsis comments + self.doComments = False # Also process comments + self.doKeywords = False # Also process keywords like tags and references self.fmtTitle = "%title%" # Formatting for titles self.fmtChapter = "%title%" # Formatting for numbered chapters @@ -153,6 +157,20 @@ class Tokenizer(): self.hideSection = hideSection return + def setFont(self, textFont, textSize, textFixed=False): + self.textFont = textFont + self.textSize = round(int(textSize)) + self.textFixed = textFixed + return + + def setLineHeight(self, lineHeight): + self.lineHeight = float(lineHeight) + return + + def setJustify(self, doJustify): + self.doJustify = doJustify + return + def setLinkHeaders(self, linkHeaders): self.linkHeaders = linkHeaders return @@ -173,10 +191,6 @@ class Tokenizer(): self.doKeywords = doKeywords return - def setJustify(self, doJustify): - self.doJustify = doJustify - return - ## # Class Methods ## diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 48c2765e..69b92157 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -62,10 +62,25 @@ class ToOdt(Tokenizer): self._autoPara = {} self._autoText = {} - self._dLanguage = "en" - self._dCountry = "GB" - self._dFontFace = "Liberation Serif" - self._dFontSize = 12 + # Properties + self.textFont = "Liberation Serif" + self.textSize = 12 + self.textFixed = False + + # Internal + self._fontFamily = None + self._fontPitch = "variable" + self._fSizeTitle = "30pt" + self._fSizeHead1 = "24pt" + self._fSizeHead2 = "20pt" + self._fSizeHead3 = "16pt" + self._fSizeHead4 = "14pt" + self._fSizeHead = "14pt" + self._fSizeText = "12pt" + self._lineHeight = "115%" + self._textAlign = "left" + self._dLanguage = "en" + self._dCountry = "GB" return @@ -86,13 +101,6 @@ class ToOdt(Tokenizer): return True - def setFont(self, fontFace, fontSize): - """Set font and font size. - """ - self._dFontFace = fontFace - self._dFontSize = fontSize - return - ## # Class Methods ## @@ -105,11 +113,29 @@ class ToOdt(Tokenizer): _mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text", } self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS) + self._xFont = etree.SubElement(self._xRoot, _mkTag("office", "font-face-decls")) self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles")) self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles")) self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body")) self._xText = etree.SubElement(self._xBody, _mkTag("office", "text")) + # Re-Init Variables + self._fontFamily = self.textFont + if len(self.textFont.split()) > 1: + self._fontFamily = f"'{self.textFont}'" + self._fontPitch = "fixed" if self.textFixed else "variable" + + self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt" + self._fSizeHead1 = f"{round(2.00 * self.textSize):d}pt" + self._fSizeHead2 = f"{round(1.60 * self.textSize):d}pt" + self._fSizeHead3 = f"{round(1.30 * self.textSize):d}pt" + self._fSizeHead4 = f"{round(1.15 * self.textSize):d}pt" + self._fSizeHead = f"{round(1.15 * self.textSize):d}pt" + self._fSizeText = f"{self.textSize:d}pt" + + self._lineHeight = f"{round(100 * self.lineHeight):d}%" + self._textAlign = "justify" if self.doJustify else "left" + # Add Styles self._defaultStyles() self._useableStyles() @@ -293,6 +319,14 @@ class ToOdt(Tokenizer): def _defaultStyles(self): """Set the default styles. """ + # Add Font + # ======== + + theAttr = {} + theAttr[_mkTag("style", "name")] = self.textFont + theAttr[_mkTag("style", "font-pitch")] = self._fontPitch + xStyl = etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=theAttr) + # Add Paragraph Family Style # ========================== @@ -307,10 +341,11 @@ class ToOdt(Tokenizer): etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) theAttr = {} - theAttr[_mkTag("style", "font-name")] = self._dFontFace - theAttr[_mkTag("fo", "font-size")] = "%dpt" % self._dFontSize - theAttr[_mkTag("fo", "language")] = self._dLanguage - theAttr[_mkTag("fo", "country")] = self._dCountry + theAttr[_mkTag("style", "font-name")] = self.textFont + theAttr[_mkTag("fo", "font-family")] = self._fontFamily + theAttr[_mkTag("fo", "font-size")] = self._fSizeText + theAttr[_mkTag("fo", "language")] = self._dLanguage + theAttr[_mkTag("fo", "country")] = self._dCountry etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) # Add Standard Paragraph Style @@ -320,7 +355,13 @@ class ToOdt(Tokenizer): theAttr[_mkTag("style", "name")] = "Standard" theAttr[_mkTag("style", "family")] = "paragraph" theAttr[_mkTag("style", "class")] = "text" - etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("style", "font-name")] = self.textFont + theAttr[_mkTag("fo", "font-family")] = self._fontFamily + theAttr[_mkTag("fo", "font-size")] = self._fSizeText + etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) # Add Default Heading Style # ========================= @@ -334,16 +375,15 @@ class ToOdt(Tokenizer): xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0.423cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" - theAttr[_mkTag("fo", "keep-with-next")] = "always" + theAttr[_mkTag("fo", "margin-top")] = "0.423cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("fo", "keep-with-next")] = "always" etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) theAttr = {} - theAttr[_mkTag("style", "font-name")] = self._dFontFace - theAttr[_mkTag("fo", "font-family")] = "'%s'" % self._dFontFace - theAttr[_mkTag("style", "font-pitch")] = "variable" - theAttr[_mkTag("fo", "font-size")] = "14pt" + theAttr[_mkTag("style", "font-name")] = self.textFont + theAttr[_mkTag("fo", "font-family")] = self._fontFamily + theAttr[_mkTag("fo", "font-size")] = self._fSizeHead etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) return @@ -360,11 +400,11 @@ class ToOdt(Tokenizer): oStyle.setClass("text") oStyle.setMarginTop("0cm") oStyle.setMarginBottom("0.247cm") - oStyle.setLineHeight("115%") - if self.doJustify: - oStyle.setTextAlign("justify") - else: - oStyle.setTextAlign("left") + oStyle.setLineHeight(self._lineHeight) + oStyle.setFontName(self.textFont) + oStyle.setFontFamily(self._fontFamily) + oStyle.setFontSize(self._fSizeText) + oStyle.setTextAlign(self._textAlign) oStyle.packXML(self._xStyl, "Text_Body") self._mainPara["Text_Body"] = oStyle @@ -378,7 +418,9 @@ class ToOdt(Tokenizer): oStyle.setNextStyleName("Text_Body") oStyle.setClass("chapter") oStyle.setTextAlign("center") - oStyle.setFontSize("28pt") + oStyle.setFontName(self.textFont) + oStyle.setFontFamily(self._fontFamily) + oStyle.setFontSize(self._fSizeTitle) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Title") @@ -395,7 +437,9 @@ class ToOdt(Tokenizer): oStyle.setClass("text") oStyle.setMarginTop("0.423cm") oStyle.setMarginBottom("0.212cm") - oStyle.setFontSize("200%") + oStyle.setFontName(self.textFont) + oStyle.setFontFamily(self._fontFamily) + oStyle.setFontSize(self._fSizeHead1) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_1") @@ -412,7 +456,9 @@ class ToOdt(Tokenizer): oStyle.setClass("text") oStyle.setMarginTop("0.353cm") oStyle.setMarginBottom("0.212cm") - oStyle.setFontSize("140%") + oStyle.setFontName(self.textFont) + oStyle.setFontFamily(self._fontFamily) + oStyle.setFontSize(self._fSizeHead2) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_2") @@ -429,7 +475,9 @@ class ToOdt(Tokenizer): oStyle.setClass("text") oStyle.setMarginTop("0.247cm") oStyle.setMarginBottom("0.212cm") - oStyle.setFontSize("125%") + oStyle.setFontName(self.textFont) + oStyle.setFontFamily(self._fontFamily) + oStyle.setFontSize(self._fSizeHead3) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_3") @@ -446,7 +494,9 @@ class ToOdt(Tokenizer): oStyle.setClass("text") oStyle.setMarginTop("0.247cm") oStyle.setMarginBottom("0.212cm") - oStyle.setFontSize("110%") + oStyle.setFontName(self.textFont) + oStyle.setFontFamily(self._fontFamily) + oStyle.setFontSize(self._fSizeHead4) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_4") @@ -491,8 +541,10 @@ class ODTParagraphStyle(): # text Attributes self._tAttr = { - "font-size": ["fo", None], - "font-weight": ["fo", None], + "font-name": ["style", None], + "font-family": ["fo", None], + "font-size": ["fo", None], + "font-weight": ["fo", None], } return @@ -558,6 +610,14 @@ class ODTParagraphStyle(): # Text Setters ## + def setFontName(self, theValue): + self._tAttr["font-name"][1] = str(theValue) + return + + def setFontFamily(self, theValue): + self._tAttr["font-family"][1] = str(theValue) + return + def setFontSize(self, theValue): self._tAttr["font-size"][1] = str(theValue) return diff --git a/nw/gui/build.py b/nw/gui/build.py index 4584d44b..9e80b588 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -35,7 +35,7 @@ from datetime import datetime from PyQt5.QtCore import Qt, QByteArray, QTimer from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtGui import ( - QPalette, QColor, QTextDocumentWriter, QFont, QCursor + QPalette, QColor, QTextDocumentWriter, QFont, QCursor, QFontInfo ) from PyQt5.QtWidgets import ( qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, @@ -607,6 +607,8 @@ class GuiBuildNovel(QDialog): fmtUnnumbered = self.fmtUnnumbered.text().strip() fmtScene = self.fmtScene.text().strip() fmtSection = self.fmtSection.text().strip() + textFont = self.textFont.text() + textSize = self.textSize.value() justifyText = self.justifyText.isChecked() noStyling = self.noStyling.isChecked() incSynopsis = self.includeSynopsis.isChecked() @@ -617,6 +619,10 @@ class GuiBuildNovel(QDialog): ignoreFlag = self.ignoreFlag.isChecked() includeBody = self.includeBody.isChecked() + # Get font information + fontInfo = QFontInfo(QFont(textFont, textSize)) + textFixed = fontInfo.fixedPitch() + isHtml = isinstance(bldObj, ToHtml) isOdt = isinstance(bldObj, ToOdt) @@ -625,11 +631,14 @@ class GuiBuildNovel(QDialog): bldObj.setUnNumberedFormat(fmtUnnumbered) bldObj.setSceneFormat(fmtScene, fmtScene == "") bldObj.setSectionFormat(fmtSection, fmtSection == "") - bldObj.setBodyText(includeBody) + + bldObj.setFont(textFont, textSize, textFixed) + bldObj.setJustify(justifyText) + bldObj.setSynopsis(incSynopsis) bldObj.setComments(incComments) bldObj.setKeywords(incKeywords) - bldObj.setJustify(justifyText) + bldObj.setBodyText(includeBody) if isHtml: bldObj.setStyles(not noStyling) From cbbe1b4f479a4ea852f4cfac687b549ddf0b5fba Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 13:36:09 +0100 Subject: [PATCH 007/104] Add ODT meta namespace, add font format to HTML, and merge page break settings --- nw/core/tohtml.py | 43 +++++++++---------- nw/core/tokenizer.py | 10 ++--- nw/core/toodt.py | 21 ++++++++- nw/gui/build.py | 2 +- .../guiBuild_Tool_Step1_Lorem_Ipsum.htm | 3 +- .../guiBuild_Tool_Step2_Lorem_Ipsum.htm | 3 +- .../guiBuild_Tool_Step3_Lorem_Ipsum.htm | 3 +- .../guiBuild_Tool_Step4H_Lorem_Ipsum.json | 5 ++- .../guiBuild_Tool_Step4_Lorem_Ipsum.htm | 3 +- tests/test_core/test_core_tohtml.py | 20 +++------ tests/test_core/test_core_tokenizer.py | 2 +- tests/test_core/test_core_toodt.py | 1 + 12 files changed, 62 insertions(+), 54 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 46c7d8b6..9c17aeca 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -173,16 +173,12 @@ class ToHtml(Tokenizer): aStyle.append("text-align: justify;") if tStyle & self.A_PBB: aStyle.append("page-break-before: always;") - if tStyle & self.A_PBB_AV: - aStyle.append("page-break-before: avoid;") - if tStyle & self.A_PBB_NO: - aStyle.append("page-break-before: never;") + if tStyle & self.A_PBB_AUT: + aStyle.append("page-break-before: auto;") if tStyle & self.A_PBA: aStyle.append("page-break-after: always;") - if tStyle & self.A_PBA_AV: - aStyle.append("page-break-after: avoid;") - if tStyle & self.A_PBA_NO: - aStyle.append("page-break-after: never;") + if tStyle & self.A_PBA_AUT: + aStyle.append("page-break-after: auto;") if len(aStyle) > 0: hStyle = " style='%s'" % (" ".join(aStyle)) @@ -268,22 +264,23 @@ class ToHtml(Tokenizer): if not self.cssStyles: return theStyles - if self.doJustify: - theStyles.append(r"p {text-align: justify;}") - else: - theStyles.append(r"p {text-align: left;}") + textAlign = "justify" if self.doJustify else "left" - theStyles.append(r"h1, h2 {color: rgb(66, 113, 174);}") - theStyles.append(r"h3, h4 {color: rgb(50, 50, 50);}") - theStyles.append(r"h1, h2, h3, h4 {page-break-after: avoid;}") - theStyles.append(r"a {color: rgb(66, 113, 174);}") - theStyles.append(r".title {font-size: 2.5em;}") - theStyles.append(r".tags {color: rgb(245, 135, 31); font-weight: bold;}") - theStyles.append(r".break {text-align: left;}") - theStyles.append(r".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}") - theStyles.append(r".skip {margin-top: 1em; margin-bottom: 1em;}") - theStyles.append(r".synopsis {font-style: italic;}") - theStyles.append(r".comment {font-style: italic; color: rgb(100, 100, 100);}") + theStyles.append("body {font-family: '%s'; font-size: %dpt}" % ( + self.textFont, self.textSize) + ) + theStyles.append("p {text-align: %s;}" % textAlign) + theStyles.append("h1, h2 {color: rgb(66, 113, 174);}") + theStyles.append("h3, h4 {color: rgb(50, 50, 50);}") + theStyles.append("h1, h2, h3, h4 {page-break-after: avoid;}") + theStyles.append("a {color: rgb(66, 113, 174);}") + theStyles.append(".title {font-size: 2.5em;}") + theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}") + theStyles.append(".break {text-align: left;}") + theStyles.append(".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}") + theStyles.append(".skip {margin-top: 1em; margin-bottom: 1em;}") + theStyles.append(".synopsis {font-style: italic;}") + theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}") return theStyles diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 4e0c6290..04ded5c1 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -67,11 +67,9 @@ class Tokenizer(): A_CENTRE = 0x0004 # Centred A_JUSTIFY = 0x0008 # Justified A_PBB = 0x0010 # Page break before always - A_PBB_AV = 0x0020 # Page break before avoid - A_PBB_NO = 0x0040 # Page break before never - A_PBA = 0x0080 # Page break after always - A_PBA_AV = 0x0100 # Page break after avoid - A_PBA_NO = 0x0200 # Page break after avoid + A_PBB_AUT = 0x0020 # Page break before auto + A_PBA = 0x0040 # Page break after always + A_PBA_AUT = 0x0080 # Page break after auto def __init__(self, theProject, theParent): @@ -628,7 +626,7 @@ class Tokenizer(): tToken[1], tToken[2], tToken[3], - self.A_PBB_NO | self.A_CENTRE + self.A_PBB_AUT | self.A_CENTRE ) else: self.theTokens[n] = ( diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 69b92157..23721e8e 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -30,6 +30,7 @@ import os from lxml import etree from hashlib import sha256 +from datetime import datetime from nw.core.tokenizer import Tokenizer @@ -39,6 +40,7 @@ XML_NS = { "office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0", "text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0", + "meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", } @@ -113,12 +115,19 @@ class ToOdt(Tokenizer): _mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text", } self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS) + self._xMeta = etree.SubElement(self._xRoot, _mkTag("office", "meta")) self._xFont = etree.SubElement(self._xRoot, _mkTag("office", "font-face-decls")) self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles")) self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles")) self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body")) self._xText = etree.SubElement(self._xBody, _mkTag("office", "text")) + # Meta Data + xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date")) + xMeta.text = datetime.now().isoformat() + xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator")) + xMeta.text = f"novelWriter/{nw.__version__}" + # Re-Init Variables self._fontFamily = self.textFont if len(self.textFont.split()) > 1: @@ -166,8 +175,12 @@ class ToOdt(Tokenizer): oStyle.setTextAlign("justify") if tStyle & self.A_PBB: oStyle.setBreakBefore("page") + if tStyle & self.A_PBB_AUT: + oStyle.setBreakBefore("auto") if tStyle & self.A_PBA: oStyle.setBreakAfter("page") + if tStyle & self.A_PBA_AUT: + oStyle.setBreakAfter("auto") # Process Text Type if tType == self.T_EMPTY: @@ -212,7 +225,7 @@ class ToOdt(Tokenizer): if parStyle is None: parStyle = oStyle # for xPos, xLen, xFmt in reversed(tFormat): - # tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:] + # tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:] if tText.endswith(" "): thisPar.append(tTemp.rstrip()+"\n") hasHardBreak = True @@ -511,6 +524,10 @@ class ToOdt(Tokenizer): # =============================================================================================== # class ODTParagraphStyle(): + """Wrapper class for the paragraph style setting used by the + exporter. Only the used settings are exposed here to keep the class + minimal and fast. + """ VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"] VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"] @@ -539,7 +556,7 @@ class ODTParagraphStyle(): "break-after": ["fo", None], } - # text Attributes + # Text Attributes self._tAttr = { "font-name": ["style", None], "font-family": ["fo", None], diff --git a/nw/gui/build.py b/nw/gui/build.py index 9e80b588..8e2b3dc7 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -573,7 +573,7 @@ class GuiBuildNovel(QDialog): self.nwdText = nwdText tEnd = int(time()) - logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) + logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart))) self.htmlStyle = makeHtml.getStyleSheet() self.buildTime = tEnd diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm index a3e4432f..70b6b8ac 100644 --- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm @@ -5,6 +5,7 @@ Lorem Ipsum
-

Lorem Ipsum

+

Lorem Ipsum

By lipsum.com

“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”

“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”

diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm index 4ab595a3..30d610db 100644 --- a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm @@ -5,6 +5,7 @@ Lorem Ipsum
-

Lorem Ipsum

+

Lorem Ipsum

By lipsum.com

“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”

“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”

diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm index 8e6ad4b5..2c385360 100644 --- a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm @@ -5,6 +5,7 @@ Lorem Ipsum
-

Lorem Ipsum

+

Lorem Ipsum

By lipsum.com

“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”

“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”

diff --git a/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json b/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json index c67a1321..ede728bb 100644 --- a/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json +++ b/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json @@ -5,10 +5,11 @@ "authors": [ "lipsum.com" ], - "buildTime": 1611662802 + "buildTime": 1611750760 }, "text": { "css": [ + "body {font-family: 'Sans'; font-size: 12pt}", "p {text-align: justify;}", "h1, h2 {color: rgb(66, 113, 174);}", "h3, h4 {color: rgb(50, 50, 50);}", @@ -24,7 +25,7 @@ ], "html": [ [ - "

Lorem Ipsum

" + "

Lorem Ipsum

" ], [ "" diff --git a/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm index f5360c30..fe3676c8 100644 --- a/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm @@ -5,6 +5,7 @@ Lorem Ipsum
-

Lorem Ipsum

+

Lorem Ipsum

Prologue

Synopsis: Explanation from the lipsum.com website.

Act One

diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index be99c340..0c20dddd 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -211,12 +211,12 @@ def testCoreToHtml_Convert(dummyGUI): # Title theHtml.theTokens = [ - (theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE), + (theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_AUT | theHtml.A_CENTRE), (theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE), ] theHtml.doConvert() assert theHtml.theResult == ( - "

" + "

" "A Title

\n" ) @@ -299,24 +299,14 @@ def testCoreToHtml_Convert(dummyGUI): "style='page-break-before: always; page-break-after: always;'>A Title\n" ) - # Page Break Avoid + # Page Break Auto theHtml.theTokens = [ - (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV), + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AUT | theHtml.A_PBA_AUT), ] theHtml.doConvert() assert theHtml.theResult == ( "

A Title

\n" - ) - - # Page Break ANever - theHtml.theTokens = [ - (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO), - ] - theHtml.doConvert() - assert theHtml.theResult == ( - "

A Title

\n" + "style='page-break-before: auto; page-break-after: auto;'>A Title\n" ) # Preview Mode diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 64096f99..ed5fdc07 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -623,7 +623,7 @@ def testCoreToken_Headers(dummyGUI): theToken.isPart = False theToken.doHeaders() assert theToken.theTokens == [ - (Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE), + (Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_AUT | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE), ] diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index c0e8c797..ae3e0d6b 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -31,6 +31,7 @@ XML_NS = [ ' 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"', + ' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"', ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"', ] From 4df294f0a2e562398769c59c24f19197489df4cc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 16:22:59 +0100 Subject: [PATCH 008/104] Add customised margin sizes --- nw/core/tokenizer.py | 33 +++++++++ nw/core/toodt.py | 108 ++++++++++++++++++++++++----- nw/gui/build.py | 1 + tests/test_core/test_core_toodt.py | 1 + 4 files changed, 127 insertions(+), 16 deletions(-) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 04ded5c1..cc6aeb58 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -95,6 +95,15 @@ class Tokenizer(): self.doComments = False # Also process comments self.doKeywords = False # Also process keywords like tags and references + ## Title Margins + self.marginTitle = (1.00, 0.50) + self.marginHead1 = (1.00, 0.50) + self.marginHead2 = (0.85, 0.50) + self.marginHead3 = (0.58, 0.50) + self.marginHead4 = (0.58, 0.50) + self.marginText = (0.00, 0.58) + + ## Title Formats self.fmtTitle = "%title%" # Formatting for titles self.fmtChapter = "%title%" # Formatting for numbered chapters self.fmtUnNum = "%title%" # Formatting for unnumbered chapters @@ -169,6 +178,30 @@ class Tokenizer(): self.doJustify = doJustify return + def setTitleMargins(self, mUpper, mLower): + self.marginTitle = (float(mUpper), float(mLower)) + return + + def setHead1Margins(self, mUpper, mLower): + self.marginHead1 = (float(mUpper), float(mLower)) + return + + def setHead2Margins(self, mUpper, mLower): + self.marginHead2 = (float(mUpper), float(mLower)) + return + + def setHead3Margins(self, mUpper, mLower): + self.marginHead3 = (float(mUpper), float(mLower)) + return + + def setHead4Margins(self, mUpper, mLower): + self.marginHead4 = (float(mUpper), float(mLower)) + return + + def setTextMargins(self, mUpper, mLower): + self.marginText = (float(mUpper), float(mLower)) + return + def setLinkHeaders(self, linkHeaders): self.linkHeaders = linkHeaders return diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 23721e8e..e557caf1 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -39,6 +39,7 @@ logger = logging.getLogger(__name__) XML_NS = { "office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0", + "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0", "text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0", "meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", @@ -65,12 +66,13 @@ class ToOdt(Tokenizer): self._autoText = {} # Properties - self.textFont = "Liberation Serif" - self.textSize = 12 - self.textFixed = False + self.textFont = "Liberation Serif" + self.textSize = 12 + self.textFixed = False + self.colourHead = False # Internal - self._fontFamily = None + self._fontFamily = "'Liberation Sans'" self._fontPitch = "variable" self._fSizeTitle = "30pt" self._fSizeHead1 = "24pt" @@ -84,6 +86,29 @@ class ToOdt(Tokenizer): self._dLanguage = "en" self._dCountry = "GB" + ## Text Margings in Units of em + self._mTopTitle = "0.423cm" + self._mTopHead1 = "0.423cm" + self._mTopHead2 = "0.353cm" + self._mTopHead3 = "0.247cm" + self._mTopHead4 = "0.247cm" + self._mTopHead = "0.423cm" + self._mTopText = "0.000cm" + + self._mBotTitle = "0.212cm" + self._mBotHead1 = "0.212cm" + self._mBotHead2 = "0.212cm" + self._mBotHead3 = "0.212cm" + self._mBotHead4 = "0.212cm" + self._mBotHead = "0.212cm" + self._mBotText = "0.247cm" + + ## Colour + self._colHead12 = None + self._opaHead12 = None + self._colHead34 = None + self._opaHead34 = None + return ## @@ -103,6 +128,10 @@ class ToOdt(Tokenizer): return True + def setColourHeaders(self, doColour): + self.colourHead = doColour + return + ## # Class Methods ## @@ -142,6 +171,28 @@ class ToOdt(Tokenizer): self._fSizeHead = f"{round(1.15 * self.textSize):d}pt" self._fSizeText = f"{self.textSize:d}pt" + self._mTopTitle = self._emToCm(self.marginTitle[0]) + self._mTopHead1 = self._emToCm(self.marginHead1[0]) + self._mTopHead2 = self._emToCm(self.marginHead2[0]) + self._mTopHead3 = self._emToCm(self.marginHead3[0]) + self._mTopHead4 = self._emToCm(self.marginHead4[0]) + self._mTopHead = self._emToCm(self.marginHead4[0]) + self._mTopText = self._emToCm(self.marginText[0]) + + self._mBotTitle = self._emToCm(self.marginTitle[1]) + self._mBotHead1 = self._emToCm(self.marginHead1[1]) + self._mBotHead2 = self._emToCm(self.marginHead2[1]) + self._mBotHead3 = self._emToCm(self.marginHead3[1]) + self._mBotHead4 = self._emToCm(self.marginHead4[1]) + self._mBotHead = self._emToCm(self.marginHead4[1]) + self._mBotText = self._emToCm(self.marginText[1]) + + if self.colourHead: + self._colHead12 = "#2a6099" + self._opaHead12 = "100%" + self._colHead34 = "#323232" + self._opaHead34 = "100%" + self._lineHeight = f"{round(100 * self.lineHeight):d}%" self._textAlign = "justify" if self.doJustify else "left" @@ -325,6 +376,11 @@ class ToOdt(Tokenizer): return newName + def _emToCm(self, emVal): + """Converts an em value to centimetres. + """ + return f"{emVal*2.54/72*self.textSize:.3f}cm" + ## # Style Elements ## @@ -388,8 +444,8 @@ class ToOdt(Tokenizer): xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "0.423cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm" + theAttr[_mkTag("fo", "margin-top")] = self._mTopHead + theAttr[_mkTag("fo", "margin-bottom")] = self._mBotHead theAttr[_mkTag("fo", "keep-with-next")] = "always" etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr) @@ -411,8 +467,8 @@ class ToOdt(Tokenizer): oStyle.setDisplayName("Text Body") oStyle.setParentStyleName("Standard") oStyle.setClass("text") - oStyle.setMarginTop("0cm") - oStyle.setMarginBottom("0.247cm") + oStyle.setMarginTop(self._mTopText) + oStyle.setMarginBottom(self._mBotText) oStyle.setLineHeight(self._lineHeight) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) @@ -431,6 +487,8 @@ class ToOdt(Tokenizer): oStyle.setNextStyleName("Text_Body") oStyle.setClass("chapter") oStyle.setTextAlign("center") + oStyle.setMarginTop(self._mTopTitle) + oStyle.setMarginBottom(self._mBotTitle) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) oStyle.setFontSize(self._fSizeTitle) @@ -448,11 +506,13 @@ class ToOdt(Tokenizer): oStyle.setNextStyleName("Text_Body") oStyle.setOutlineLevel("1") oStyle.setClass("text") - oStyle.setMarginTop("0.423cm") - oStyle.setMarginBottom("0.212cm") + oStyle.setMarginTop(self._mTopHead1) + oStyle.setMarginBottom(self._mBotHead1) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) oStyle.setFontSize(self._fSizeHead1) + oStyle.setColor(self._colHead12) + oStyle.setOpacity(self._opaHead12) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_1") @@ -467,11 +527,13 @@ class ToOdt(Tokenizer): oStyle.setNextStyleName("Text_Body") oStyle.setOutlineLevel("2") oStyle.setClass("text") - oStyle.setMarginTop("0.353cm") - oStyle.setMarginBottom("0.212cm") + oStyle.setMarginTop(self._mTopHead2) + oStyle.setMarginBottom(self._mBotHead2) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) oStyle.setFontSize(self._fSizeHead2) + oStyle.setColor(self._colHead12) + oStyle.setOpacity(self._opaHead12) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_2") @@ -486,11 +548,13 @@ class ToOdt(Tokenizer): oStyle.setNextStyleName("Text_Body") oStyle.setOutlineLevel("3") oStyle.setClass("text") - oStyle.setMarginTop("0.247cm") - oStyle.setMarginBottom("0.212cm") + oStyle.setMarginTop(self._mTopHead3) + oStyle.setMarginBottom(self._mBotHead3) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) oStyle.setFontSize(self._fSizeHead3) + oStyle.setColor(self._colHead34) + oStyle.setOpacity(self._opaHead34) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_3") @@ -505,11 +569,13 @@ class ToOdt(Tokenizer): oStyle.setNextStyleName("Text_Body") oStyle.setOutlineLevel("4") oStyle.setClass("text") - oStyle.setMarginTop("0.247cm") - oStyle.setMarginBottom("0.212cm") + oStyle.setMarginTop(self._mTopHead4) + oStyle.setMarginBottom(self._mBotHead4) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) oStyle.setFontSize(self._fSizeHead4) + oStyle.setColor(self._colHead34) + oStyle.setOpacity(self._opaHead34) oStyle.setFontWeight("bold") oStyle.packXML(self._xStyl, "Heading_4") @@ -562,6 +628,8 @@ class ODTParagraphStyle(): "font-family": ["fo", None], "font-size": ["fo", None], "font-weight": ["fo", None], + "color": ["fo", None], + "opacity": ["loext", None], } return @@ -644,6 +712,14 @@ class ODTParagraphStyle(): self._tAttr["font-weight"][1] = str(theValue) return + def setColor(self, theValue): + self._tAttr["color"][1] = str(theValue) + return + + def setOpacity(self, theValue): + self._tAttr["opacity"][1] = str(theValue) + return + ## # Getters ## diff --git a/nw/gui/build.py b/nw/gui/build.py index 8e2b3dc7..09d7d7e9 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -644,6 +644,7 @@ class GuiBuildNovel(QDialog): bldObj.setStyles(not noStyling) if isOdt: + bldObj.setColourHeaders(not noStyling) bldObj.initDocument() # Make sure the tree order is correct diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index ae3e0d6b..300fdd6b 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -30,6 +30,7 @@ from nw.core import NWProject, NWIndex, ToOdt XML_NS = [ ' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"', ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"', + ' xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"', ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"', ' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"', ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"', From 8715ab9322439d361bb2d27f9fbdc5a4d2f9954f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 19:36:34 +0100 Subject: [PATCH 009/104] Add logic to apply formatting to text --- nw/core/tohtml.py | 6 +- nw/core/toodt.py | 239 ++++++++++++++++++++++++----- sample/content/636b6aa9b697b.nwd | 2 +- tests/test_core/test_core_toodt.py | 41 +++++ 4 files changed, 246 insertions(+), 42 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 9c17aeca..ab7e0c81 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -236,12 +236,12 @@ class ToHtml(Tokenizer): if parStyle is None: parStyle = hStyle for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:] + tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:] if tText.endswith(" "): - thisPar.append(tTemp.rstrip()+"
") + thisPar.append(tTemp.rstrip() + "
") hasHardBreak = True else: - thisPar.append(tTemp.rstrip()+" ") + thisPar.append(tTemp.rstrip() + " ") elif tType == self.T_SYNOPSIS and self.doSynopsis: tmpResult.append(self._formatSynopsis(tText)) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index e557caf1..2ea5e378 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -26,7 +26,6 @@ along with this program. If not, see . import nw import logging -import os from lxml import etree from hashlib import sha256 @@ -45,11 +44,14 @@ XML_NS = { "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", } -X_BR = "{%s}line-break" % XML_NS["text"] -X_TAB = "{%s}tab" % XML_NS["text"] - class ToOdt(Tokenizer): + X_BLD = 0x01 + X_ITA = 0x02 + X_DEL = 0x04 + X_BRK = 0x08 + X_TAB = 0x10 + def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) @@ -208,7 +210,17 @@ class ToOdt(Tokenizer): """ self.theResult = "" + odtTags = { + self.FMT_B_B : "_B", + self.FMT_B_E : "b_", + self.FMT_I_B : "I", + self.FMT_I_E : "i", + self.FMT_D_B : "_S", + self.FMT_D_E : "s_", + } + thisPar = [] + thisFmt = [] parStyle = None hasHardBreak = False for tType, tLine, tText, tFormat, tStyle in self.theTokens: @@ -238,10 +250,16 @@ class ToOdt(Tokenizer): if hasHardBreak and parStyle is not None: if self.doJustify: parStyle.setTextAlign("left") + if len(thisPar) > 0: tTemp = "".join(thisPar) - self._addTextPar("Text_Body", parStyle, tTemp.rstrip()) + fTemp = "".join(thisFmt) + tTxt = tTemp.rstrip() + tFmt = fTemp[:len(tTxt)] + self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt) + thisPar = [] + thisFmt = [] parStyle = None hasHardBreak = False @@ -275,21 +293,31 @@ class ToOdt(Tokenizer): tTemp = tText if parStyle is None: parStyle = oStyle - # for xPos, xLen, xFmt in reversed(tFormat): - # tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:] + + tFmt = " "*len(tTemp) + for xPos, xLen, xFmt in tFormat: + tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:] + + tTxt = tTemp.rstrip() + tFmt = tFmt[:len(tTxt)] if tText.endswith(" "): - thisPar.append(tTemp.rstrip()+"\n") + thisPar.append(tTxt + "\n") + thisFmt.append(tFmt + " ") hasHardBreak = True else: - thisPar.append(tTemp.rstrip()+" ") + thisPar.append(tTxt + " ") + thisFmt.append(tFmt + " ") return def closeDocument(self): """Return the serialised XML document """ + # Build the auto-generated styles for styleName, styleObj in self._autoPara.values(): styleObj.packXML(self._xAuto, styleName) + for styleName, styleObj in self._autoText.values(): + styleObj.packXML(self._xAuto, styleName) self.theResult = etree.tostring( self._xRoot, @@ -298,17 +326,13 @@ class ToOdt(Tokenizer): xml_declaration = True ) - cacheFile = os.path.join(os.path.expanduser("~"), "Temp", "odtGen.fodt") - with open(cacheFile, mode="wb") as outFile: - outFile.write(self.theResult) - return ## # Internal Functions ## - def _addTextPar(self, styleName, oStyle, theText, isHead=False, oLevel=None): + def _addTextPar(self, styleName, oStyle, theText, theFmt="", isHead=False, oLevel=None): """Add a text paragraph to the text XML element. """ tAttr = {} @@ -322,36 +346,85 @@ class ToOdt(Tokenizer): if not theText: return - if "\t" not in theText and "\n" not in theText: - xElem.text = theText - return + ## + # Process Formatting + ## - # Process tabs and line breaks - tTemp = "" + if len(theText) != len(theFmt): + # Genrate dummu format if there isn't any + theFmt = " "*len(theText) + + # XML functions xTail = None - for c in theText: - if c == "\t": + + def appendText(tText): + nonlocal xElem, xTail + if tText: if xTail is None: - xElem.text = tTemp + xElem.text = tText else: - xTail.tail = tTemp - tTemp = "" - xTail = etree.SubElement(xElem, X_TAB) - elif c == "\n": - if xTail is None: - xElem.text = tTemp - else: - xTail.tail = tTemp - tTemp = "" - xTail = etree.SubElement(xElem, X_BR) - else: + xTail.tail = tText + + def appendSpan(tText, tFmt): + nonlocal xElem, xTail + if tText: + xTail = etree.SubElement(xElem, _mkTag("text", "span"), attrib={ + _mkTag("text", "style-name"): self._textStyle(tFmt) + }) + xTail.text = tText + + # The formatting loop + tTemp = "" + xFmt = 0x00 + pFmt = 0x00 + + for i, c in enumerate(theText): + + if theFmt[i] == "_": + continue + elif theFmt[i] == "B": + xFmt |= self.X_BLD + elif theFmt[i] == "b": + xFmt ^= self.X_BLD + elif theFmt[i] == "I": + xFmt |= self.X_ITA + elif theFmt[i] == "i": + xFmt ^= self.X_ITA + elif theFmt[i] == "S": + xFmt |= self.X_DEL + elif theFmt[i] == "s": + xFmt ^= self.X_DEL + + if c == "\n": + xFmt |= self.X_BRK + c = "" + elif c == "\t": + xFmt |= self.X_TAB + c = "" + + if theFmt[i] == " ": tTemp += c - if tTemp != "": - if xTail is None: - xElem.text = tTemp - else: - xTail.tail = tTemp + if xFmt != pFmt: + if pFmt == 0x00: + appendText(tTemp) + tTemp = "" + else: + appendSpan(tTemp, pFmt) + tTemp = "" + + if xFmt & self.X_BRK: + xTail = etree.SubElement(xElem, _mkTag("text", "line-break")) + xFmt ^= self.X_BRK + + if xFmt & self.X_TAB: + xTail = etree.SubElement(xElem, _mkTag("text", "tab")) + xFmt ^= self.X_TAB + + pFmt = xFmt + + # Save what remains in the buffer + appendText(tTemp) return @@ -376,6 +449,26 @@ class ToOdt(Tokenizer): return newName + def _textStyle(self, styleCode): + """Return a text style for a given style code. + """ + if styleCode in self._autoText: + return self._autoText[styleCode][0] + + newName = "T%d" % (len(self._autoText) + 1) + newStyle = ODTTextStyle() + if styleCode & self.X_BLD: + newStyle.setFontWeight("bold") + if styleCode & self.X_ITA: + newStyle.setFontStyle("italic") + if styleCode & self.X_DEL: + newStyle.setStrikeStyle("solid") + newStyle.setStrikeType("single") + + self._autoText[styleCode] = (newName, newStyle) + + return newName + def _emToCm(self, emVal): """Converts an em value to centimetres. """ @@ -803,6 +896,76 @@ class ODTParagraphStyle(): # END Class ODTParagraphStyle +class ODTTextStyle(): + """Wrapper class for the text style setting used by the exporter. + Only the used settings are exposed here to keep the class minimal + and fast. + """ + VALID_WEIGHT = ["normal", "inherit", "bold"] + VALID_STYLE = ["normal", "inherit", "italic"] + VALID_LSTYLE = ["none", "solid"] + VALID_LTYPE = ["none", "single", "double"] + + def __init__(self): + + # Text Attributes + self._tAttr = { + "font-weight": ["fo", None], + "font-style": ["fo", None], + "text-line-through-style": ["style", None], + "text-line-through-type": ["style", None], + } + + return + + ## + # Setters + ## + + def setFontWeight(self, theValue): + if theValue in self.VALID_WEIGHT: + self._tAttr["font-weight"][1] = str(theValue) + return + + def setFontStyle(self, theValue): + if theValue in self.VALID_STYLE: + self._tAttr["font-style"][1] = str(theValue) + return + + def setStrikeStyle(self, theValue): + if theValue in self.VALID_LSTYLE: + self._tAttr["text-line-through-style"][1] = str(theValue) + return + + def setStrikeType(self, theValue): + if theValue in self.VALID_LTYPE: + self._tAttr["text-line-through-type"][1] = str(theValue) + return + + ## + # Methods + ## + + def packXML(self, xParent, xName): + """Pack the content into an xml element. + """ + theAttr = {} + theAttr[_mkTag("style", "name")] = xName + theAttr[_mkTag("style", "family")] = "text" + xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr) + + theAttr = {} + for aName, (aNm, aVal) in self._tAttr.items(): + if aVal is not None: + theAttr[_mkTag(aNm, aName)] = aVal + + if theAttr: + etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr) + + return + +# END Class ODTTextStyle + # =============================================================================================== # # Local Functions # =============================================================================================== # diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 6a1302ef..be611dce 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -9,7 +9,7 @@ A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. -Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isn’t fully Markdown compliant. If the syntax highlighter doesn’t show it correctly, the export tool will not either. +Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isn’t fully Markdown compliant. If the syntax highlighter doesn’t show it correctly, the export tool will not either. In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 300fdd6b..ecf1af8d 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -83,4 +83,45 @@ def testCoreToOdt_Convert(tmpConf, dummyGUI): '' ) + # Nested Text + theDoc.theText = "Some ~~nested **bold** and _italics_ text~~ text.\nNo format\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert xmlToText(theDoc._xText) == ( + '' + 'Some ' + 'nested ' + 'bold' + ' and ' + 'italics' + ' text text. No format' + '' + ) + + # Hard Break + theDoc.theText = "Some text. \nNext line\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert xmlToText(theDoc._xText) == ( + '' + 'Some text.Next line' + '' + ) + + # Tab + theDoc.theText = "\tItem 1\tItem 2\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert xmlToText(theDoc._xText) == ( + '' + 'Item 1Item 2' + '' + ) + # END Test testCoreToOdt_Convert From d83352e16f3c641a49024d84949a87e94f3dcc43 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 Jan 2021 19:49:21 +0100 Subject: [PATCH 010/104] Fix Build Novel Project test on Ubuntu --- tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm | 2 +- tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm | 2 +- tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm | 2 +- tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json | 2 +- tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm | 2 +- tests/test_gui/test_gui_build.py | 3 +++ 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm index 70b6b8ac..50696774 100644 --- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm @@ -5,7 +5,7 @@ Lorem Ipsum \n" + "\n" + "
\n" + "{bodyText:s}\n" + "
\n" + "\n" + "\n" + ).format( + projTitle = self.theProject.projName, + htmlStyle = "\n".join(theStyle), + bodyText = bodyText, + ) + outFile.write(theHtml) + + return + + def replaceTabs(self, nSpaces=8, spaceChar=" "): + """Replace tabs with spaces in the html. + """ + htmlText = [] + eightSpace = spaceChar*nSpaces + for aLine in self.fullHTML: + htmlText.append(aLine.replace("\t", eightSpace)) + + self.fullHTML = htmlText return def getStyleSheet(self): diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index cc6aeb58..a1ffe11f 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -70,6 +70,8 @@ class Tokenizer(): A_PBB_AUT = 0x0020 # Page break before auto A_PBA = 0x0040 # Page break after always A_PBA_AUT = 0x0080 # Page break after auto + A_Z_TOPMRG = 0x0100 # Zero top margin + A_Z_BTMMRG = 0x0200 # Zero bottom margin def __init__(self, theProject, theParent): @@ -77,12 +79,14 @@ class Tokenizer(): self.theParent = theParent # Data Variables - self.theText = None # The raw text to be tokenized + self.theText = "" # The raw text to be tokenized self.theHandle = None # The handle associated with the text self.theItem = None # The NWItem associated with the handle - self.theTokens = None # The list of the processed tokens - self.theResult = None # The result text after conversion - self.theMarkdown = None # The result text in novelWriter markdown + self.theTokens = [] # The list of the processed tokens + self.theResult = "" # The result of the last document + + self.keepMarkdown = False # Whether to keep the markdown text + self.theMarkdown = [] # The result novelWriter markdown of all documents # User Settings self.textFont = "Serif" # Output text font @@ -96,12 +100,13 @@ class Tokenizer(): self.doKeywords = False # Also process keywords like tags and references ## Title Margins - self.marginTitle = (1.00, 0.50) - self.marginHead1 = (1.00, 0.50) - self.marginHead2 = (0.85, 0.50) - self.marginHead3 = (0.58, 0.50) - self.marginHead4 = (0.58, 0.50) - self.marginText = (0.00, 0.58) + self.marginTitle = (1.000, 0.500) + self.marginHead1 = (1.000, 0.500) + self.marginHead2 = (0.834, 0.500) + self.marginHead3 = (0.584, 0.500) + self.marginHead4 = (0.584, 0.500) + self.marginText = (0.000, 0.584) + self.marginMeta = (0.000, 0.584) ## Title Formats self.fmtTitle = "%title%" # Formatting for titles @@ -202,6 +207,10 @@ class Tokenizer(): self.marginText = (float(mUpper), float(mLower)) return + def setMetaMargins(self, mUpper, mLower): + self.marginMeta = (float(mUpper), float(mLower)) + return + def setLinkHeaders(self, linkHeaders): self.linkHeaders = linkHeaders return @@ -222,6 +231,10 @@ class Tokenizer(): self.doKeywords = doKeywords return + def setKeepMarkdown(self, keepMarkdown): + self.keepMarkdown = keepMarkdown + return + ## # Class Methods ## @@ -241,7 +254,8 @@ class Tokenizer(): self.theTokens.append(( self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE )) - self.theMarkdown = "# %s\n\n" % theTitle + if self.keepMarkdown: + self.theMarkdown.append("# %s\n\n" % theTitle) return True @@ -283,23 +297,6 @@ class Tokenizer(): return True - def getResult(self): - """Return the result from the conversion. - """ - return self.theResult - - def getResultSize(self): - """Return the size of the result from the conversion. - """ - if self.theResult is None: - return 0 - return len(self.theResult) - - def getFilteredMarkdown(self): - """Return the novelWriter markdown after the filters have been applied. - """ - return self.theMarkdown - def doAutoReplace(self): """Run through the user's auto-replace dictionary. """ @@ -352,7 +349,6 @@ class Tokenizer(): ] self.theTokens = [] - self.theMarkdown = "" tmpMarkdown = [] nLine = 0 for aLine in self.theText.splitlines(): @@ -361,88 +357,61 @@ class Tokenizer(): # Tag lines starting with specific characters if len(aLine.strip()) == 0: self.theTokens.append(( - self.T_EMPTY, - nLine, - "", - None, - self.A_NONE + self.T_EMPTY, nLine, "", None, self.A_NONE )) - tmpMarkdown.append("\n") + if self.keepMarkdown: + tmpMarkdown.append("\n") elif aLine[0] == "%": cLine = aLine[1:].lstrip() synTag = cLine[:9].lower() if synTag == "synopsis:": self.theTokens.append(( - self.T_SYNOPSIS, - nLine, - cLine[9:].strip(), - None, - self.A_NONE + self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE )) - if self.doSynopsis: + if self.doSynopsis and self.keepMarkdown: tmpMarkdown.append("%s\n" % aLine) else: self.theTokens.append(( - self.T_COMMENT, - nLine, - aLine[1:].strip(), - None, - self.A_NONE + self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE )) - if self.doComments: + if self.doComments and self.keepMarkdown: tmpMarkdown.append("%s\n" % aLine) elif aLine[0] == "@": self.theTokens.append(( - self.T_KEYWORD, - nLine, - aLine[1:].strip(), - None, - self.A_NONE + self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE )) - if self.doKeywords: + if self.doKeywords and self.keepMarkdown: tmpMarkdown.append("%s\n" % aLine) elif aLine[:2] == "# ": self.theTokens.append(( - self.T_HEAD1, - nLine, - aLine[2:].strip(), - None, - self.A_NONE + self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE )) - tmpMarkdown.append("%s\n" % aLine) + if self.keepMarkdown: + tmpMarkdown.append("%s\n" % aLine) elif aLine[:3] == "## ": self.theTokens.append(( - self.T_HEAD2, - nLine, - aLine[3:].strip(), - None, - self.A_NONE + self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE )) - tmpMarkdown.append("%s\n" % aLine) + if self.keepMarkdown: + tmpMarkdown.append("%s\n" % aLine) elif aLine[:4] == "### ": self.theTokens.append(( - self.T_HEAD3, - nLine, - aLine[4:].strip(), - None, - self.A_NONE + self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE )) - tmpMarkdown.append("%s\n" % aLine) + if self.keepMarkdown: + tmpMarkdown.append("%s\n" % aLine) elif aLine[:5] == "#### ": self.theTokens.append(( - self.T_HEAD4, - nLine, - aLine[5:].strip(), - None, - self.A_NONE + self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE )) - tmpMarkdown.append("%s\n" % aLine) + if self.keepMarkdown: + tmpMarkdown.append("%s\n" % aLine) else: if not self.doBodyText: @@ -465,26 +434,44 @@ class Tokenizer(): # sorted by position fmtPos = sorted(fmtPos, key=itemgetter(0)) self.theTokens.append(( - self.T_TEXT, - nLine, - aLine, - fmtPos, - self.A_NONE + self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE )) - tmpMarkdown.append("%s\n" % aLine) + if self.keepMarkdown: + tmpMarkdown.append("%s\n" % aLine) # Always add an empty line at the end self.theTokens.append(( - self.T_EMPTY, - nLine, - "", - None, - self.A_NONE + self.T_EMPTY, nLine, "", None, self.A_NONE )) - tmpMarkdown.append("\n") + if self.keepMarkdown: + tmpMarkdown.append("\n") - self.theMarkdown = "".join(tmpMarkdown) - tmpMarkdown = [] + if self.keepMarkdown: + self.theMarkdown.append("".join(tmpMarkdown)) + + # Second Pass + # =========== + # Some items need a second pass + + pToken = (self.T_EMPTY, 0, "", None, self.A_NONE) + nToken = (self.T_EMPTY, 0, "", None, self.A_NONE) + tCount = len(self.theTokens) + for n, tToken in enumerate(self.theTokens): + + if n > 0: + pToken = self.theTokens[n-1] + if n < tCount - 1: + nToken = self.theTokens[n+1] + + if tToken[0] == self.T_KEYWORD: + aStyle = tToken[4] + if pToken[0] == self.T_KEYWORD: + aStyle |= self.A_Z_TOPMRG + if nToken[0] == self.T_KEYWORD: + aStyle |= self.A_Z_BTMMRG + self.theTokens[n] = ( + tToken[0], tToken[1], tToken[2], tToken[3], aStyle + ) return @@ -499,9 +486,7 @@ class Tokenizer(): # For novel files, we need to handle chapter numbering, scene # numbering, and scene breaks if self.isNovel: - for n in range(len(self.theTokens)): - - tToken = self.theTokens[n] + for n, tToken in enumerate(self.theTokens): # In case we see text before a scene, we reset the flag if tToken[0] == self.T_TEXT: @@ -513,11 +498,7 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtTitle, tToken[2]) self.theTokens[n] = ( - tToken[0], - tToken[1], - tTemp, - None, - self.A_NONE + tToken[0], tToken[1], tTemp, None, self.A_NONE ) elif tToken[0] == self.T_HEAD2: @@ -535,11 +516,7 @@ class Tokenizer(): # Format the chapter header self.theTokens[n] = ( - tToken[0], - tToken[1], - tTemp, - None, - self.A_PBB + tToken[0], tToken[1], tTemp, None, self.A_PBB ) # Set scene variables @@ -556,53 +533,29 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtScene, tToken[2]) if tTemp == "" and self.hideScene: self.theTokens[n] = ( - self.T_EMPTY, - tToken[1], - "", - None, - self.A_NONE + self.T_EMPTY, tToken[1], "", None, self.A_NONE ) elif tTemp == "" and not self.hideScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, - tToken[1], - "", - None, - self.A_NONE + self.T_EMPTY, tToken[1], "", None, self.A_NONE ) else: self.theTokens[n] = ( - self.T_SKIP, - tToken[1], - "", - None, - self.A_NONE + self.T_SKIP, tToken[1], "", None, self.A_NONE ) elif tTemp == self.fmtScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, - tToken[1], - "", - None, - self.A_NONE + self.T_EMPTY, tToken[1], "", None, self.A_NONE ) else: self.theTokens[n] = ( - self.T_SEP, - tToken[1], - tTemp, - None, - self.A_CENTRE + self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE ) else: self.theTokens[n] = ( - tToken[0], - tToken[1], - tTemp, - None, - self.A_NONE + tToken[0], tToken[1], tTemp, None, self.A_NONE ) # Definitely no longer the first scene @@ -615,35 +568,19 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtSection, tToken[2]) if tTemp == "" and self.hideSection: self.theTokens[n] = ( - self.T_EMPTY, - tToken[1], - "", - None, - self.A_NONE + self.T_EMPTY, tToken[1], "", None, self.A_NONE ) elif tTemp == "" and not self.hideSection: self.theTokens[n] = ( - self.T_SKIP, - tToken[1], - "", - None, - self.A_NONE + self.T_SKIP, tToken[1], "", None, self.A_NONE ) elif tTemp == self.fmtSection: self.theTokens[n] = ( - self.T_SEP, - tToken[1], - tTemp, - None, - self.A_CENTRE + self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE ) else: self.theTokens[n] = ( - tToken[0], - tToken[1], - tTemp, - None, - self.A_NONE + tToken[0], tToken[1], tTemp, None, self.A_NONE ) # For title page and partitions, we need to centre all text. @@ -654,28 +591,18 @@ class Tokenizer(): for n, tToken in enumerate(self.theTokens): if tToken[0] == self.T_HEAD1: if self.isTitle: + aStyle = self.A_PBB_AUT | self.A_CENTRE self.theTokens[n] = ( - self.T_TITLE, - tToken[1], - tToken[2], - tToken[3], - self.A_PBB_AUT | self.A_CENTRE + self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle ) else: + aStyle = self.A_PBB | self.A_CENTRE self.theTokens[n] = ( - tToken[0], - tToken[1], - tToken[2], - tToken[3], - self.A_PBB | self.A_CENTRE + tToken[0], tToken[1], tToken[2], tToken[3], aStyle ) else: self.theTokens[n] = ( - tToken[0], - tToken[1], - tToken[2], - tToken[3], - self.A_CENTRE + tToken[0], tToken[1], tToken[2], tToken[3], self.A_CENTRE ) # Add a page break after the last entry @@ -683,11 +610,7 @@ class Tokenizer(): if n >= 0: tToken = self.theTokens[n] self.theTokens[n] = ( - tToken[0], - tToken[1], - tToken[2], - tToken[3], - tToken[4] | self.A_PBA + tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA ) # A single page is always left-aligned and starts on a fresh @@ -696,19 +619,11 @@ class Tokenizer(): for n, tToken in enumerate(self.theTokens): if n == 0: self.theTokens[n] = ( - tToken[0], - tToken[1], - tToken[2], - tToken[3], - self.A_LEFT | self.A_PBB + tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT | self.A_PBB ) else: self.theTokens[n] = ( - tToken[0], - tToken[1], - tToken[2], - tToken[3], - self.A_LEFT + tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT ) return True diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 4eb8d57b..936c7faf 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -116,6 +116,7 @@ class ToOdt(Tokenizer): self._mTopHead4 = "0.247cm" self._mTopHead = "0.423cm" self._mTopText = "0.000cm" + self._mTopMeta = "0.000cm" self._mBotTitle = "0.212cm" self._mBotHead1 = "0.212cm" @@ -124,6 +125,7 @@ class ToOdt(Tokenizer): self._mBotHead4 = "0.212cm" self._mBotHead = "0.212cm" self._mBotText = "0.247cm" + self._mBotMeta = "0.106cm" ## Colour self._colHead12 = None @@ -170,7 +172,7 @@ class ToOdt(Tokenizer): self._fontFamily = self.textFont if len(self.textFont.split()) > 1: - self._fontFamily = f"'{self.textFont}'" + self._fontFamily = f"'{self.textFont}'" self._fontPitch = "fixed" if self.textFixed else "variable" self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt" @@ -188,6 +190,7 @@ class ToOdt(Tokenizer): self._mTopHead4 = self._emToCm(self.marginHead4[0]) self._mTopHead = self._emToCm(self.marginHead4[0]) self._mTopText = self._emToCm(self.marginText[0]) + self._mTopMeta = self._emToCm(self.marginMeta[0]) self._mBotTitle = self._emToCm(self.marginTitle[1]) self._mBotHead1 = self._emToCm(self.marginHead1[1]) @@ -196,13 +199,14 @@ class ToOdt(Tokenizer): self._mBotHead4 = self._emToCm(self.marginHead4[1]) self._mBotHead = self._emToCm(self.marginHead4[1]) self._mBotText = self._emToCm(self.marginText[1]) + self._mBotMeta = self._emToCm(self.marginMeta[1]) if self.colourHead: self._colHead12 = "#2a6099" self._opaHead12 = "100%" self._colHead34 = "#444444" self._opaHead34 = "100%" - self._colMetaTx = "#666666" + self._colMetaTx = "#813709" self._opaMetaTx = "100%" self._lineHeight = f"{round(100 * self.lineHeight):d}%" @@ -302,21 +306,28 @@ class ToOdt(Tokenizer): if tStyle is not None: if tStyle & self.A_LEFT: oStyle.setTextAlign("left") - if tStyle & self.A_RIGHT: + elif tStyle & self.A_RIGHT: oStyle.setTextAlign("right") - if tStyle & self.A_CENTRE: + elif tStyle & self.A_CENTRE: oStyle.setTextAlign("center") - if tStyle & self.A_JUSTIFY: + elif tStyle & self.A_JUSTIFY: oStyle.setTextAlign("justify") + if tStyle & self.A_PBB: oStyle.setBreakBefore("page") - if tStyle & self.A_PBB_AUT: + elif tStyle & self.A_PBB_AUT: oStyle.setBreakBefore("auto") + if tStyle & self.A_PBA: oStyle.setBreakAfter("page") - if tStyle & self.A_PBA_AUT: + elif tStyle & self.A_PBA_AUT: oStyle.setBreakAfter("auto") + if tStyle & self.A_Z_BTMMRG: + oStyle.setMarginBottom("0.000cm") + if tStyle & self.A_Z_TOPMRG: + oStyle.setMarginTop("0.000cm") + # Process Text Types if tType == self.T_EMPTY: if hasHardBreak and parStyle is not None: @@ -740,8 +751,8 @@ class ToOdt(Tokenizer): oStyle.setDisplayName("Text Meta") oStyle.setParentStyleName("Standard") oStyle.setClass("text") - oStyle.setMarginTop(self._mTopText) - oStyle.setMarginBottom(self._mBotText) + oStyle.setMarginTop(self._mTopMeta) + oStyle.setMarginBottom(self._mBotMeta) oStyle.setLineHeight(self._lineHeight) oStyle.setFontName(self.textFont) oStyle.setFontFamily(self._fontFamily) diff --git a/nw/gui/build.py b/nw/gui/build.py index 56190cec..d05aa0cc 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -79,7 +79,6 @@ class GuiBuildNovel(QDialog): self.htmlText = [] # List of html documents self.htmlStyle = [] # List of html styles - self.nwdText = [] # List of markdown documents self.htmlSize = 0 # Size of the html document self.buildTime = 0 # The timestamp of the last build @@ -532,7 +531,6 @@ class GuiBuildNovel(QDialog): else: self.htmlText = [] self.htmlStyle = [] - self.nwdText = [] self.buildTime = 0 return False @@ -552,34 +550,26 @@ class GuiBuildNovel(QDialog): textSize = self.textSize.value() replaceTabs = self.replaceTabs.isChecked() - tStart = int(time()) - self.htmlText = [] self.htmlStyle = [] - self.nwdText = [] self.htmlSize = 0 + # Build Preview + # ============= + makeHtml = ToHtml(self.theProject, self.theParent) - self._doBuild(makeHtml) - + self._doBuild(makeHtml, isPreview=True) if replaceTabs: - htmlText = [] - eightSpace = " "*8 - for aLine in self.htmlText: - htmlText.append(aLine.replace("\t", eightSpace)) - self.htmlText = htmlText + makeHtml.replaceTabs() - nwdText = [] - for aLine in self.nwdText: - nwdText.append(aLine.replace("\t", " ")) - self.nwdText = nwdText - - tEnd = int(time()) - logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart))) + self.htmlText = makeHtml.fullHTML self.htmlStyle = makeHtml.getStyleSheet() - self.buildTime = tEnd + self.htmlSize = makeHtml.getFullResultSize() + self.buildTime = int(time()) + + # Load Preview + # ============ - # Load the preview document with the html data self.docView.setTextFont(textFont, textSize) self.docView.setJustify(justifyText) if noStyling: @@ -600,9 +590,11 @@ class GuiBuildNovel(QDialog): return - def _doBuild(self, bldObj): + def _doBuild(self, bldObj, isPreview=False, doConvert=True): """Rund the build with a specific build object. """ + tStart = int(time()) + # Get Settings fmtTitle = self.fmtTitle.text().strip() fmtChapter = self.fmtChapter.text().strip() @@ -644,7 +636,6 @@ class GuiBuildNovel(QDialog): if isHtml: bldObj.setStyles(not noStyling) - self.htmlSize = 0 if isOdt: bldObj.setColourHeaders(not noStyling) @@ -667,31 +658,27 @@ class GuiBuildNovel(QDialog): if noteRoot: # Add headers for root folders of notes bldObj.addRootHeading(tItem.itemHandle) - bldObj.doConvert() - if isHtml: - self.htmlText.append(bldObj.getResult()) - self.nwdText.append(bldObj.getFilteredMarkdown()) - self.htmlSize += bldObj.getResultSize() + if doConvert: + bldObj.doConvert() elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): bldObj.setText(tItem.itemHandle) bldObj.doAutoReplace() bldObj.tokenizeText() bldObj.doHeaders() - bldObj.doConvert() + if doConvert: + bldObj.doConvert() bldObj.doPostProcessing() - if isHtml: - self.htmlText.append(bldObj.getResult()) - self.nwdText.append(bldObj.getFilteredMarkdown()) - self.htmlSize += bldObj.getResultSize() except Exception as e: logger.error("Failed to generate html of document '%s'" % tItem.itemHandle) logger.error(str(e)) - self.docView.setText(( - "Failed to generate preview. " - "Document with title '%s' could not be parsed." - ) % tItem.itemName) + if isPreview: + self.docView.setText(( + "Failed to generate preview. " + "Document with title '%s' could not be parsed." + ) % tItem.itemName) + return False # Update progress bar, also for skipped items @@ -700,6 +687,9 @@ class GuiBuildNovel(QDialog): if isOdt: bldObj.closeDocument() + tEnd = int(time()) + logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart))) + if bldObj.errData: self.theParent.makeAlert(( "There were problems when building the project:" @@ -751,63 +741,59 @@ class GuiBuildNovel(QDialog): def _saveDocument(self, theFormat): """Save the document to various formats. """ + replaceTabs = self.replaceTabs.isChecked() + byteFmt = QByteArray() fileExt = "" textFmt = "" - outTool = "" - # Create the settings + # Settings + # ======== + if theFormat == self.FMT_ODT: fileExt = "odt" textFmt = "Open Document" - outTool = "NW_ODT" elif theFormat == self.FMT_FODT: fileExt = "fodt" textFmt = "Flat Open Document" - outTool = "NW_ODT" elif theFormat == self.FMT_PDF: fileExt = "pdf" textFmt = "PDF" - outTool = "QtPrint" elif theFormat == self.FMT_HTM: fileExt = "htm" textFmt = "Plain HTML" - outTool = "NW" elif theFormat == self.FMT_MD: byteFmt.append("markdown") fileExt = "md" textFmt = "Markdown" - outTool = "Qt" elif theFormat == self.FMT_NWD: fileExt = "nwd" textFmt = "%s Markdown" % nw.__package__ - outTool = "NW" elif theFormat == self.FMT_TXT: byteFmt.append("plaintext") fileExt = "txt" textFmt = "Plain Text" - outTool = "Qt" elif theFormat == self.FMT_JSON_H: fileExt = "json" textFmt = "JSON + %s HTML" % nw.__package__ - outTool = "NW" elif theFormat == self.FMT_JSON_M: fileExt = "json" textFmt = "JSON + %s Markdown" % nw.__package__ - outTool = "NW" else: return False - # Generate the file name + # Generate File Name + # ================== + if fileExt: cleanName = makeFileNameSafe(self.theProject.projName) @@ -830,109 +816,109 @@ class GuiBuildNovel(QDialog): else: return False - # Do the actual writing - wSuccess = False + # Build and Write + # =============== + errMsg = "" - if outTool == "Qt": + wSuccess = False + + if theFormat == self.FMT_MD or theFormat == self.FMT_TXT: docWriter = QTextDocumentWriter() docWriter.setFileName(savePath) docWriter.setFormat(byteFmt) wSuccess = docWriter.write(self.docView.qDocument) - elif outTool == "NW": + elif theFormat == self.FMT_HTM: + makeHtml = ToHtml(self.theProject, self.theParent) + self._doBuild(makeHtml) + if replaceTabs: + makeHtml.replaceTabs() + try: - with open(savePath, mode="w", encoding="utf8") as outFile: - if theFormat == self.FMT_HTM: - # Write novelWriter HTML data - theStyle = self.htmlStyle.copy() - theStyle.append(r"article {width: 800px; margin: 40px auto;}") - bodyText = "".join(self.htmlText) - bodyText = bodyText.replace("\t", " ") - - theHtml = ( - "\n" - "\n" - "\n" - "\n" - "{projTitle:s}\n" - "\n" - "\n" - "\n" - "
\n" - "{bodyText:s}\n" - "
\n" - "\n" - "\n" - ).format( - projTitle = self.theProject.projName, - htmlStyle = "\n".join(theStyle), - bodyText = bodyText, - ) - outFile.write(theHtml) - - elif theFormat == self.FMT_NWD: - # Write novelWriter markdown data - for aLine in self.nwdText: - outFile.write(aLine) - - elif theFormat == self.FMT_JSON_H or theFormat == self.FMT_JSON_M: - jsonData = { - "meta" : { - "workingTitle" : self.theProject.projName, - "novelTitle" : self.theProject.bookTitle, - "authors" : self.theProject.bookAuthors, - "buildTime" : self.buildTime, - } - } - - if theFormat == self.FMT_JSON_H: - theBody = [] - for htmlPage in self.htmlText: - theBody.append(htmlPage.rstrip("\n").split("\n")) - jsonData["text"] = { - "css" : self.htmlStyle, - "html" : theBody, - } - elif theFormat == self.FMT_JSON_M: - theBody = [] - for nwdPage in self.nwdText: - theBody.append(nwdPage.split("\n")) - jsonData["text"] = { - "nwd" : theBody, - } - - outFile.write(json.dumps(jsonData, indent=2)) - + makeHtml.saveHTML5(savePath) wSuccess = True - except Exception as e: errMsg = str(e) - elif outTool == "NW_ODT": + elif theFormat == self.FMT_NWD: + makeNwd = ToHtml(self.theProject, self.theParent) + makeNwd.setKeepMarkdown(True) + self._doBuild(makeNwd, doConvert=False) + if replaceTabs: + makeNwd.replaceTabs(spaceChar=" ") - if theFormat == self.FMT_FODT: - makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True) - self._doBuild(makeOdt) - try: - makeOdt.saveFlatXML(savePath) + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + for nwdPage in makeNwd.theMarkdown: + outFile.write(nwdPage) + wSuccess = True + except Exception as e: + errMsg = str(e) + + elif theFormat == self.FMT_FODT: + makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True) + self._doBuild(makeOdt) + try: + makeOdt.saveFlatXML(savePath) + wSuccess = True + except Exception as e: + errMsg = str(e) + + elif theFormat == self.FMT_ODT: + makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False) + self._doBuild(makeOdt) + try: + makeOdt.saveOpenDocText(savePath) + wSuccess = True + except Exception as e: + errMsg = str(e) + + elif theFormat == self.FMT_JSON_H or theFormat == self.FMT_JSON_M: + jsonData = { + "meta" : { + "workingTitle" : self.theProject.projName, + "novelTitle" : self.theProject.bookTitle, + "authors" : self.theProject.bookAuthors, + "buildTime" : self.buildTime, + } + } + + if theFormat == self.FMT_JSON_H: + makeHtml = ToHtml(self.theProject, self.theParent) + self._doBuild(makeHtml) + if replaceTabs: + makeHtml.replaceTabs() + + theBody = [] + for htmlPage in makeHtml.fullHTML: + theBody.append(htmlPage.rstrip("\n").split("\n")) + jsonData["text"] = { + "css" : self.htmlStyle, + "html" : theBody, + } + + elif theFormat == self.FMT_JSON_M: + makeNwd = ToHtml(self.theProject, self.theParent) + makeNwd.setKeepMarkdown(True) + self._doBuild(makeNwd, doConvert=False) + if replaceTabs: + makeNwd.replaceTabs(spaceChar=" ") + + theBody = [] + for nwdPage in makeNwd.theMarkdown: + theBody.append(nwdPage.split("\n")) + jsonData["text"] = { + "nwd" : theBody, + } + + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + outFile.write(json.dumps(jsonData, indent=2)) wSuccess = True + except Exception as e: + errMsg = str(e) - except Exception as e: - errMsg = str(e) - - elif theFormat == self.FMT_ODT: - makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False) - self._doBuild(makeOdt) - try: - makeOdt.saveOpenDocText(savePath) - wSuccess = True - - except Exception as e: - errMsg = str(e) - - elif outTool == "QtPrint" and theFormat == self.FMT_PDF: + elif theFormat == self.FMT_PDF: try: thePrinter = QPrinter() thePrinter.setOutputFormat(QPrinter.PdfFormat) @@ -1021,13 +1007,10 @@ class GuiBuildNovel(QDialog): if "htmlStyle" in theData.keys(): self.htmlStyle = theData["htmlStyle"] dataCount += 1 - if "nwdText" in theData.keys(): - self.nwdText = theData["nwdText"] - dataCount += 1 if "buildTime" in theData.keys(): self.buildTime = theData["buildTime"] - return dataCount == 3 + return dataCount == 2 def _saveCache(self): """Save the current data to cache. @@ -1040,7 +1023,6 @@ class GuiBuildNovel(QDialog): outFile.write(json.dumps({ "htmlText" : self.htmlText, "htmlStyle" : self.htmlStyle, - "nwdText" : self.nwdText, "buildTime" : self.buildTime, }, indent=2)) except Exception as e: From 83e07ec76c1f1bea62c1802114edc7f0033cb55f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Jan 2021 17:04:33 +0100 Subject: [PATCH 018/104] Fix tests --- tests/test_core/test_core_tohtml.py | 9 ++--- tests/test_core/test_core_tokenizer.py | 46 ++++++++++++-------------- tests/test_gui/test_gui_build.py | 2 -- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index 0c20dddd..247e9bf5 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -332,6 +332,7 @@ def testCoreToHtml_Methods(dummyGUI): """ theProject = NWProject(dummyGUI) theHtml = ToHtml(theProject, dummyGUI) + theHtml.setKeepMarkdown(True) # Auto-Replace docText = "Text with & short–dash, long—dash …\n" @@ -344,11 +345,11 @@ def testCoreToHtml_Methods(dummyGUI): ) # Revert on MD - assert theHtml.theMarkdown == ( + assert theHtml.theMarkdown[-1] == ( "Text with <brackets> & short–dash, long—dash …\n\n" ) theHtml.doPostProcessing() - assert theHtml.theMarkdown == docText + "\n" + assert theHtml.theMarkdown[-1] == docText + "\n" # With Preview, No Revert theHtml.setPreview(True, True) @@ -356,11 +357,11 @@ def testCoreToHtml_Methods(dummyGUI): theHtml.doAutoReplace() theHtml.tokenizeText() theHtml.doConvert() - assert theHtml.theMarkdown == ( + assert theHtml.theMarkdown[-1] == ( "Text with <brackets> & short–dash, long—dash …\n\n" ) theHtml.doPostProcessing() - assert theHtml.theMarkdown == ( + assert theHtml.theMarkdown[-1] == ( "Text with <brackets> & short–dash, long—dash …\n\n" ) diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index ed5fdc07..db6b6220 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -84,6 +84,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI): theProject = NWProject(dummyGUI) theProject.projTree.setSeed(42) theToken = Tokenizer(theProject, dummyGUI) + theToken.setKeepMarkdown(True) assert theProject.openProject(nwMinimal) sHandle = "8c659a11cd429" @@ -112,7 +113,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI): assert theToken.addRootHeading("dummy") is False assert theToken.addRootHeading(sHandle) is False assert theToken.addRootHeading("7695ce551d265") is True - assert theToken.theMarkdown == "# Notes: Plot\n\n" + assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n" # Set text assert theToken.setText("dummy") is False @@ -145,12 +146,6 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI): theToken.doAutoReplace() assert theToken.theText == docTextR - # Access - assert theToken.getResult() is None - assert theToken.getResultSize() == 0 - theToken.theResult = "" - assert theToken.getResultSize() == 0 - # Post Processing theToken.theResult = r"This is text with escapes: \** \~~ \__" theToken.doPostProcessing() @@ -164,6 +159,7 @@ def testCoreToken_Tokenize(dummyGUI): """ theProject = NWProject(dummyGUI) theToken = Tokenizer(theProject, dummyGUI) + theToken.setKeepMarkdown(True) # Header 1 theToken.theText = "# Novel Title\n" @@ -172,7 +168,7 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "# Novel Title\n\n" + assert theToken.theMarkdown[-1] == "# Novel Title\n\n" # Header 2 theToken.theText = "## Chapter One\n" @@ -181,7 +177,7 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "## Chapter One\n\n" + assert theToken.theMarkdown[-1] == "## Chapter One\n\n" # Header 3 theToken.theText = "### Scene One\n" @@ -190,7 +186,7 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "### Scene One\n\n" + assert theToken.theMarkdown[-1] == "### Scene One\n\n" # Header 4 theToken.theText = "#### A Section\n" @@ -199,7 +195,7 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "#### A Section\n\n" + assert theToken.theMarkdown[-1] == "#### A Section\n\n" # Comment theToken.theText = "% A comment\n" @@ -208,11 +204,11 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "\n" + assert theToken.theMarkdown[-1] == "\n" theToken.setComments(True) theToken.tokenizeText() - assert theToken.theMarkdown == "% A comment\n\n" + assert theToken.theMarkdown[-1] == "% A comment\n\n" # Symopsis theToken.theText = "%synopsis: The synopsis\n" @@ -227,11 +223,11 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "\n" + assert theToken.theMarkdown[-1] == "\n" theToken.setSynopsis(True) theToken.tokenizeText() - assert theToken.theMarkdown == "% synopsis: The synopsis\n\n" + assert theToken.theMarkdown[-1] == "% synopsis: The synopsis\n\n" # Keyword theToken.theText = "@char: Bod\n" @@ -240,11 +236,11 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "\n" + assert theToken.theMarkdown[-1] == "\n" theToken.setKeywords(True) theToken.tokenizeText() - assert theToken.theMarkdown == "@char: Bod\n\n" + assert theToken.theMarkdown[-1] == "@char: Bod\n\n" # Text theToken.theText = "Some plain text\non two lines\n\n\n" @@ -256,7 +252,7 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "Some plain text\non two lines\n\n\n\n" + assert theToken.theMarkdown[-1] == "Some plain text\non two lines\n\n\n\n" theToken.setBodyText(False) theToken.tokenizeText() @@ -265,7 +261,7 @@ def testCoreToken_Tokenize(dummyGUI): (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "\n\n\n" + assert theToken.theMarkdown[-1] == "\n\n\n" theToken.setBodyText(True) # Text Emphasis @@ -283,7 +279,7 @@ def testCoreToken_Tokenize(dummyGUI): ), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "Some **bolded text** on this lines\n\n" + assert theToken.theMarkdown[-1] == "Some **bolded text** on this lines\n\n" theToken.theText = "Some _italic text_ on this lines\n" theToken.tokenizeText() @@ -299,7 +295,7 @@ def testCoreToken_Tokenize(dummyGUI): ), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "Some _italic text_ on this lines\n\n" + assert theToken.theMarkdown[-1] == "Some _italic text_ on this lines\n\n" theToken.theText = "Some **_bold italic text_** on this lines\n" theToken.tokenizeText() @@ -317,7 +313,7 @@ def testCoreToken_Tokenize(dummyGUI): ), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "Some **_bold italic text_** on this lines\n\n" + assert theToken.theMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n" theToken.theText = "Some ~~strikethrough text~~ on this lines\n" theToken.tokenizeText() @@ -333,7 +329,7 @@ def testCoreToken_Tokenize(dummyGUI): ), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == "Some ~~strikethrough text~~ on this lines\n\n" + assert theToken.theMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n" theToken.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" theToken.tokenizeText() @@ -353,12 +349,12 @@ def testCoreToken_Tokenize(dummyGUI): ), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown == ( + assert theToken.theMarkdown[-1] == ( "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" ) # Check the markdown function as well - assert theToken.getFilteredMarkdown() == ( + assert theToken.theMarkdown[-1] == ( "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" ) diff --git a/tests/test_gui/test_gui_build.py b/tests/test_gui/test_gui_build.py index b9bcff15..38813f00 100644 --- a/tests/test_gui/test_gui_build.py +++ b/tests/test_gui/test_gui_build.py @@ -208,7 +208,6 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): # Close the build tool htmlText = nwBuild.htmlText htmlStyle = nwBuild.htmlStyle - nwdText = nwBuild.nwdText buildTime = nwBuild.buildTime nwBuild._doClose() @@ -222,7 +221,6 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): assert nwBuild.viewCachedDoc() assert nwBuild.htmlText == htmlText assert nwBuild.htmlStyle == htmlStyle - assert nwBuild.nwdText == nwdText assert nwBuild.buildTime == buildTime nwBuild._doClose() From 7254f766d5e630eb3a6e674ae05fe2a05436b074 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Jan 2021 20:42:39 +0100 Subject: [PATCH 019/104] Remove div tags from HTML output and bring Tokenizer and ToHtml tests back to 100% --- nw/core/tohtml.py | 7 +- tests/test_core/test_core_tohtml.py | 109 +++++++++++++++++++++++-- tests/test_core/test_core_tokenizer.py | 50 +++++++++++- 3 files changed, 152 insertions(+), 14 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 980ac6e6..1a5b345c 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -267,7 +267,8 @@ class ToHtml(Tokenizer): tmpResult.append(self._formatComments(tText)) elif tType == self.T_KEYWORD and self.doKeywords: - tmpResult.append(self._formatKeywords(tText)) + tTemp = "%s

\n" % (hStyle, self._formatKeywords(tText)) + tmpResult.append(tTemp) self.theResult = "".join(tmpResult) tmpResult = [] @@ -284,7 +285,7 @@ class ToHtml(Tokenizer): theStyle = self.getStyleSheet() theStyle.append("article {width: 800px; margin: 40px auto;}") bodyText = "".join(self.fullHTML) - bodyText = bodyText.replace("\t", " ") + bodyText = bodyText.replace("\t", " ").rstrip() theHtml = ( "\n" @@ -399,7 +400,7 @@ class ToHtml(Tokenizer): )) retText += ", ".join(refTags) - return "
%s
\n" % retText + return retText def _buildRegEx(self): """Build the regular expressions diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index 247e9bf5..e55b2980 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -20,8 +20,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os import pytest +from tools import readFile + from nw.core import NWProject, NWIndex, ToHtml @pytest.mark.core @@ -44,14 +47,12 @@ def testCoreToHtml_Format(dummyGUI): assert theHtml._formatKeywords("") == "" assert theHtml._formatKeywords("tag: Jane") == ( - "
Tag: Jane
\n" + "Tag: Jane" ) assert theHtml._formatKeywords("char: Bod, Jane") == ( - "
" "Characters: " "Bod, " "Jane" - "
\n" ) # Preview Mode @@ -68,14 +69,12 @@ def testCoreToHtml_Format(dummyGUI): assert theHtml._formatKeywords("") == "" assert theHtml._formatKeywords("tag: Jane") == ( - "
Tag: Jane
\n" + "Tag: Jane" ) assert theHtml._formatKeywords("char: Bod, Jane") == ( - "
" "Characters: " "Bod, " "Jane" - "
\n" ) # END Test testCoreToHtml_Format @@ -200,8 +199,27 @@ def testCoreToHtml_Convert(dummyGUI): theHtml.tokenizeText() theHtml.doConvert() assert theHtml.theResult == ( - "
Characters: " - "Bod, Jane
\n" + "

Characters: " + "Bod, Jane

\n" + ) + + # Multiple Keywords + theHtml.setKeywords(True) + theHtml.theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

" + "Chapter

\n" + "

" + "Point of View: Bod" + "

\n" + "

" + "Plot: Main" + "

\n" + "

" + "Locations: Europe" + "

\n" ) # Direct Tests @@ -326,6 +344,78 @@ def testCoreToHtml_Convert(dummyGUI): # END Test testCoreToHtml_Convert +def testCoreToHtml_Complex(dummyGUI, fncDir): + """Test the ave method of the ToHtml class. + """ + theProject = NWProject(dummyGUI) + theHtml = ToHtml(theProject, dummyGUI) + + # Build Project + # ============= + + docText = [ + "# My Novel\n**By Jane Doh**\n", + "## Chapter 1\n\nThe text of chapter one.\n", + "### Scene 1\n\nThe text of scene one.\n", + "#### A Section\n\nMore text in scene one.\n", + "## Chapter 2\n\nThe text of chapter two.\n", + "### Scene 2\n\nThe text of scene two.\n", + "#### A Section\n\n\tMore text in scene two.\n", + ] + resText = [ + "

My Novel

\n

By Jane Doh

\n", + "

Chapter 1

\n

The text of chapter one.

\n", + "

Scene 1

\n

The text of scene one.

\n", + "

A Section

\n

More text in scene one.

\n", + "

Chapter 2

\n

The text of chapter two.

\n", + "

Scene 2

\n

The text of scene two.

\n", + "

A Section

\n

\tMore text in scene two.

\n", + ] + + for i in range(len(docText)): + theHtml.theText = docText[i] + theHtml.doAutoReplace() + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == resText[i] + + assert theHtml.fullHTML == resText + + theHtml.replaceTabs(nSpaces=2, spaceChar=" ") + resText[6] = "

A Section

\n

  More text in scene two.

\n" + + # Check File + # ========== + + theStyle = theHtml.getStyleSheet() + theStyle.append("article {width: 800px; margin: 40px auto;}") + htmlDoc = ( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
\n" + "{bodyText:s}\n" + "
\n" + "\n" + "\n" + ).format( + htmlStyle = "\n".join(theStyle), + bodyText = "".join(resText).rstrip() + ) + + saveFile = os.path.join(fncDir, "outFile.htm") + theHtml.saveHTML5(saveFile) + assert readFile(saveFile) == htmlDoc + +# END Test testCoreToHtml_Save + @pytest.mark.core def testCoreToHtml_Methods(dummyGUI): """Test all the other methods of the ToHtml class. @@ -365,6 +455,9 @@ def testCoreToHtml_Methods(dummyGUI): "Text with <brackets> & short–dash, long—dash …\n\n" ) + # Result Size + assert theHtml.getFullResultSize() == 83 + # CSS # === diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index db6b6220..6cee910f 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -38,6 +38,18 @@ def testCoreToken_Setters(dummyGUI): assert theToken.fmtUnNum == "%title%" assert theToken.fmtScene == "%title%" assert theToken.fmtSection == "%title%" + assert theToken.textFont == "Serif" + assert theToken.textSize == 11 + assert theToken.textFixed is False + assert theToken.lineHeight == 1.15 + assert theToken.doJustify is False + assert theToken.marginTitle == (1.000, 0.500) + assert theToken.marginHead1 == (1.000, 0.500) + assert theToken.marginHead2 == (0.834, 0.500) + assert theToken.marginHead3 == (0.584, 0.500) + assert theToken.marginHead4 == (0.584, 0.500) + assert theToken.marginText == (0.000, 0.584) + assert theToken.marginMeta == (0.000, 0.584) assert theToken.hideScene is False assert theToken.hideSection is False assert theToken.linkHeaders is False @@ -45,7 +57,6 @@ def testCoreToken_Setters(dummyGUI): assert theToken.doSynopsis is False assert theToken.doComments is False assert theToken.doKeywords is False - assert theToken.doJustify is False # Set new values theToken.setTitleFormat("T: %title%") @@ -53,12 +64,21 @@ def testCoreToken_Setters(dummyGUI): theToken.setUnNumberedFormat("U: %title%") theToken.setSceneFormat("S: %title%", True) theToken.setSectionFormat("X: %title%", True) + theToken.setFont("Monospace", 10, True) + theToken.setLineHeight(2) + theToken.setJustify(True) + theToken.setTitleMargins(2.0, 2.0) + theToken.setHead1Margins(2.0, 2.0) + theToken.setHead2Margins(2.0, 2.0) + theToken.setHead3Margins(2.0, 2.0) + theToken.setHead4Margins(2.0, 2.0) + theToken.setTextMargins(2.0, 2.0) + theToken.setMetaMargins(2.0, 2.0) theToken.setLinkHeaders(True) theToken.setBodyText(False) theToken.setSynopsis(True) theToken.setComments(True) theToken.setKeywords(True) - theToken.setJustify(True) # Check new values assert theToken.fmtTitle == "T: %title%" @@ -66,6 +86,18 @@ def testCoreToken_Setters(dummyGUI): assert theToken.fmtUnNum == "U: %title%" assert theToken.fmtScene == "S: %title%" assert theToken.fmtSection == "X: %title%" + assert theToken.textFont == "Monospace" + assert theToken.textSize == 10 + assert theToken.textFixed is True + assert theToken.lineHeight == 2.0 + assert theToken.doJustify is True + assert theToken.marginTitle == (2.0, 2.0) + assert theToken.marginHead1 == (2.0, 2.0) + assert theToken.marginHead2 == (2.0, 2.0) + assert theToken.marginHead3 == (2.0, 2.0) + assert theToken.marginHead4 == (2.0, 2.0) + assert theToken.marginText == (2.0, 2.0) + assert theToken.marginMeta == (2.0, 2.0) assert theToken.hideScene is True assert theToken.hideSection is True assert theToken.linkHeaders is True @@ -73,7 +105,6 @@ def testCoreToken_Setters(dummyGUI): assert theToken.doSynopsis is True assert theToken.doComments is True assert theToken.doKeywords is True - assert theToken.doJustify is True # END Test testCoreToken_Setters @@ -242,6 +273,19 @@ def testCoreToken_Tokenize(dummyGUI): theToken.tokenizeText() assert theToken.theMarkdown[-1] == "@char: Bod\n\n" + theToken.theText = "@pov: Bod\n@plot: Main\n@location: Europe\n" + theToken.tokenizeText() + styTop = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG + styMid = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG | Tokenizer.A_Z_TOPMRG + styBtm = Tokenizer.A_NONE | Tokenizer.A_Z_TOPMRG + assert theToken.theTokens == [ + (Tokenizer.T_KEYWORD, 1, "pov: Bod", None, styTop), + (Tokenizer.T_KEYWORD, 2, "plot: Main", None, styMid), + (Tokenizer.T_KEYWORD, 3, "location: Europe", None, styBtm), + (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown[-1] == "@pov: Bod\n@plot: Main\n@location: Europe\n\n" + # Text theToken.theText = "Some plain text\non two lines\n\n\n" theToken.tokenizeText() From 1ff7c8bdd503057f42b7312ba942683c59b4c0b7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Jan 2021 20:48:49 +0100 Subject: [PATCH 020/104] Update reference files for build test --- .../guiBuild_Tool_Step1_Lorem_Ipsum.htm | 1 - .../guiBuild_Tool_Step2_Lorem_Ipsum.htm | 49 +++++++++---------- .../guiBuild_Tool_Step3_Lorem_Ipsum.htm | 49 +++++++++---------- .../guiBuild_Tool_Step4H_Lorem_Ipsum.json | 44 ++++++++--------- .../guiBuild_Tool_Step4_Lorem_Ipsum.htm | 43 ++++++++-------- 5 files changed, 91 insertions(+), 95 deletions(-) diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm index 50696774..413f48df 100644 --- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm @@ -66,7 +66,6 @@ article {width: 800px; margin: 40px auto;}

Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis.

Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue.

Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.

-
diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm index 053bfeff..24ab442f 100644 --- a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm @@ -35,15 +35,15 @@ article {width: 800px; margin: 40px auto;}

Act One

“Fusce maximus felis libero”

Chapter One: Chapter One

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar.

Scene 1.1: Scene One

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.

Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada.

Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex.

@@ -51,9 +51,9 @@ article {width: 800px; margin: 40px auto;}

Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst.

Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend.

Scene 1.2: Scene Two

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien.

Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis.

@@ -68,24 +68,24 @@ article {width: 800px; margin: 40px auto;}

The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.

Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).

Chapter Three: Chapter Two

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.

Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at.

Scene 3.1: Scene Three

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis.

Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis.

Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus.

Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac.

Scene 3.2: Scene Four

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.

Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien.

Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum.

@@ -94,9 +94,9 @@ article {width: 800px; margin: 40px auto;}

Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui.

Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero.

Scene 3.3: Scene Five

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet.

In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere.

@@ -105,22 +105,21 @@ article {width: 800px; margin: 40px auto;}

Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.

Notes: Characters

Nobody Owens

-
Tag: Bod
+

Tag: Bod

Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.

Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.

Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.

Notes: Plot

Main Plot

-
Tag: Main
+

Tag: Main

Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.

Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.

Notes: World

Ancient Europe

-
Tag: Europe
+

Tag: Europe

Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.

Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.

Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim.

-
diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm index 5ef73fbb..e98adbb2 100644 --- a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm @@ -35,15 +35,15 @@ article {width: 800px; margin: 40px auto;}

Act One

“Fusce maximus felis libero”

Chapter One: Chapter One

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar.

Scene 1.1: Scene One

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.

Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada.

Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex.

@@ -51,9 +51,9 @@ article {width: 800px; margin: 40px auto;}

Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst.

Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend.

Scene 1.2: Scene Two

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien.

Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis.

@@ -68,24 +68,24 @@ article {width: 800px; margin: 40px auto;}

        The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.

        Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).

Chapter Three: Chapter Two

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.

Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at.

Scene 3.1: Scene Three

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis.

Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis.

Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus.

Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac.

Scene 3.2: Scene Four

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.

Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien.

Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum.

@@ -94,9 +94,9 @@ article {width: 800px; margin: 40px auto;}

Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui.

Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero.

Scene 3.3: Scene Five

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet.

In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere.

@@ -105,22 +105,21 @@ article {width: 800px; margin: 40px auto;}

Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.

Notes: Characters

Nobody Owens

-
Tag: Bod
+

Tag: Bod

Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.

Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.

Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.

Notes: Plot

Main Plot

-
Tag: Main
+

Tag: Main

Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.

Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.

Notes: World

Ancient Europe

-
Tag: Europe
+

Tag: Europe

Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.

Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.

Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim.

-
diff --git a/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json b/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json index 510c3db0..23f3d6b9 100644 --- a/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json +++ b/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json @@ -5,7 +5,7 @@ "authors": [ "lipsum.com" ], - "buildTime": 1611750760 + "buildTime": 1611863279 }, "text": { "css": [ @@ -39,53 +39,53 @@ ], [ "

Chapter One: Chapter One

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.

" ], [ "

Scene 1: Scene One

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.

", "

Section: Scene One, Section Two

" ], [ "

Scene 2: Scene Two

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.

", "

Section: Scene Two, Section Two

" ], [ "

Chapter Two: Chapter Two

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.

" ], [ "

Scene 3: Scene Three

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

" ], [ "

Scene 4: Scene Four

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.

" ], [ "

Scene 5: Scene Five

", - "
Point of View: Bod
", - "
Plot: Main
", - "
Locations: Europe
", + "

Point of View: Bod

", + "

Plot: Main

", + "

Locations: Europe

", "

Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

" ] ] diff --git a/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm index afe03352..ed51c450 100644 --- a/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm @@ -27,43 +27,42 @@ article {width: 800px; margin: 40px auto;}

Synopsis: Explanation from the lipsum.com website.

Act One

Chapter One: Chapter One

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.

Scene 1: Scene One

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.

Section: Scene One, Section Two

Scene 2: Scene Two

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.

Section: Scene Two, Section Two

Chapter Two: Chapter Two

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.

Scene 3: Scene Three

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

Scene 4: Scene Four

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.

Scene 5: Scene Five

-
Point of View: Bod
-
Plot: Main
-
Locations: Europe
+

Point of View: Bod

+

Plot: Main

+

Locations: Europe

Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

-
From 6f7cf4f186ace32d5b1923c1abd739e324e090da Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Jan 2021 22:09:28 +0100 Subject: [PATCH 021/104] Bump dev version to 1.2-alpha0 --- CHANGELOG.md | 4 ++++ docs/source/conf.py | 4 ++-- nw/__init__.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 502c19fa..e78f376a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # novelWriter Change Log +## Version 1.2 Dev (Alpha) + +---- + ## Version 1.1 Dev (Alpha) ### Release Notes diff --git a/docs/source/conf.py b/docs/source/conf.py index ae98916c..eb35d492 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -25,9 +25,9 @@ copyright = "2018–2021, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "1.1" +version = "1.2" # The full version, including alpha/beta/rc tags -release = "1.1-alpha0" +release = "1.2-alpha0" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index a513224d..02ac7162 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -63,9 +63,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "1.1a0" -__hexversion__ = "0x010100a0" -__date__ = "2020-12-13" +__version__ = "1.2a0" +__hexversion__ = "0x010200a0" +__date__ = "2020-02-01" __status__ = "Unstable" __domain__ = "novelwriter.io" __url__ = "https://novelwriter.io" From 59cfb14a1961d2fbcd59be5f138ec796f75bd8ec Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 00:57:34 +0100 Subject: [PATCH 022/104] Add a first title index that tracks the level and location of the first title of a file --- nw/core/index.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index ba2e06f3..a4c013a1 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -57,6 +57,7 @@ class NWIndex(): self._novelIndex = {} self._noteIndex = {} self._textCounts = {} + self._firstTitle = {} # TimeStamps self._timeNovel = 0 @@ -77,6 +78,7 @@ class NWIndex(): self._novelIndex = {} self._noteIndex = {} self._textCounts = {} + self._firstTitle = {} self._timeNovel = 0 self._timeNotes = 0 self._timeIndex = 0 @@ -99,6 +101,7 @@ class NWIndex(): self._novelIndex.pop(tHandle, None) self._noteIndex.pop(tHandle, None) self._textCounts.pop(tHandle, None) + self._firstTitle.pop(tHandle, None) return @@ -162,6 +165,7 @@ class NWIndex(): self._novelIndex = theData.get("novelIndex", {}) self._noteIndex = theData.get("noteIndex", {}) self._textCounts = theData.get("textCounts", {}) + self._firstTitle = theData.get("firstTitle", {}) nowTime = round(time()) self._timeNovel = nowTime @@ -187,6 +191,7 @@ class NWIndex(): "novelIndex" : self._novelIndex, "noteIndex" : self._noteIndex, "textCounts" : self._textCounts, + "firstTitle" : self._firstTitle, }, outFile, indent=2) except Exception as e: logger.error("Failed to save index file") @@ -227,7 +232,13 @@ class NWIndex(): if len(self._textCounts[tHandle]) != 3: self.indexBroken = True - except Exception: + for tHandle in self._firstTitle: + if len(self._firstTitle[tHandle]) != 2: + self.indexBroken = True + + except Exception as e: + logger.error("Error while checking index") + logger.error(str(e)) self.indexBroken = True logger.debug("Index check complete") @@ -290,6 +301,7 @@ class NWIndex(): "tags" : [], "updated" : round(time()), } + self._firstTitle[tHandle] = ["H0", "T000000"] if itemLayout == nwItemLayout.NOTE: self._novelIndex.pop(tHandle, None) self._noteIndex[tHandle] = {} @@ -317,7 +329,7 @@ class NWIndex(): if nChar == 0: continue - if aLine.startswith(r"#"): + if aLine.startswith("#"): isTitle = self._indexTitle(tHandle, isNovel, aLine, nLine, itemLayout) if isTitle and nLine > 0: if nTitle > 0: @@ -325,11 +337,11 @@ class NWIndex(): self._indexWordCounts(tHandle, isNovel, lastText, nTitle) nTitle = nLine - elif aLine.startswith(r"@"): + elif aLine.startswith("@"): self._indexNoteRef(tHandle, aLine, nLine, nTitle) self._indexTag(tHandle, aLine, nLine, nTitle, itemClass) - elif aLine.startswith(r"%"): + elif aLine.startswith("%"): if nTitle > 0: toCheck = aLine[1:].lstrip() synTag = toCheck[:9].lower() @@ -398,6 +410,9 @@ class NWIndex(): "updated" : round(time()), } + if self._firstTitle[tHandle][0] == "H0": + self._firstTitle[tHandle] = [hDepth, sTitle] + if hText != "": if isNovel: if tHandle in self._novelIndex: @@ -643,6 +658,11 @@ class NWIndex(): return theToC + def getFirstTitle(self, tHandle): + """Return the level and location of the first title of a handle. + """ + return self._firstTitle.get(tHandle, ["H0", "T000000"]) + def getCounts(self, tHandle, sTitle=None): """Returns the counts for a file, or a section of a file starting at title sTitle if it is provided. From 4c27d2600aa1598e32b4fc77d8423d959c0df80d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 01:09:04 +0100 Subject: [PATCH 023/104] Update document layout during editor save process --- nw/core/tree.py | 48 ++++++++++++++++++++++++++++++++++++++++++++- nw/gui/doceditor.py | 8 +++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/nw/core/tree.py b/nw/core/tree.py index 36a56e5c..786ba89f 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -33,10 +33,36 @@ from time import time from nw.core.item import NWItem from nw.common import checkHandle -from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst +from nw.constants import ( + nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst, nwLists +) logger = logging.getLogger(__name__) +# Translation map for item layouts +NOVEL_MAP = { + nwItemLayout.SCENE: { + "H1": nwItemLayout.PARTITION, + "H2": nwItemLayout.CHAPTER, + "H4": nwItemLayout.SCENE, + }, + nwItemLayout.CHAPTER: { + "H1": nwItemLayout.PARTITION, + "H3": nwItemLayout.SCENE, + "H4": nwItemLayout.SCENE, + }, + nwItemLayout.UNNUMBERED: { + "H1": nwItemLayout.PARTITION, + "H3": nwItemLayout.SCENE, + "H4": nwItemLayout.SCENE, + }, + nwItemLayout.PARTITION: { + "H2": nwItemLayout.CHAPTER, + "H3": nwItemLayout.SCENE, + "H4": nwItemLayout.SCENE, + }, +} + class NWTree(): def __init__(self, theProject): @@ -202,6 +228,26 @@ class NWTree(): novelWords += tItem.wordCount return novelWords, noteWords + def updateItemLayout(self, tHandle, hLevel): + """Check if the item layout needs updating based on the header + given level. + """ + tItem = self.__getitem__(tHandle) + if tItem is None: + return False + if tItem.itemClass not in nwLists.CLS_NOVEL: + return False + if hLevel not in ("H1", "H2", "H3", "H4"): + return False + + iLayout = tItem.itemLayout + if iLayout in NOVEL_MAP: + if hLevel in NOVEL_MAP[iLayout]: + tItem.itemLayout = NOVEL_MAP[iLayout][hLevel] + return True + + return False + ## # Tree Structure Methods ## diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index b3709420..9990988a 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -393,6 +393,7 @@ class GuiDocEditor(QTextEdit): return False docText = self.getText() + tHandle = theItem.itemHandle cC, wC, pC = countWords(docText) self._updateCounts(cC, wC, pC) @@ -405,7 +406,12 @@ class GuiDocEditor(QTextEdit): self.nwDocument.saveDocument(docText) self.setDocumentChanged(False) - self.theParent.theIndex.scanText(theItem.itemHandle, docText) + self.theParent.theIndex.scanText(tHandle, docText) + + hLevel, _ = self.theParent.theIndex.getFirstTitle(tHandle) + if self.theProject.projTree.updateItemLayout(tHandle, hLevel): + self.theParent.treeView.setTreeItemValues(tHandle) + self.nwDocument.saveDocument(docText) return True From 8de0c55b6849a369b1dc1914430bd026aa0f23ab Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 01:14:58 +0100 Subject: [PATCH 024/104] Fix broken tests --- nw/core/tree.py | 8 +-- .../coreIndex_LoadSave_tagsIndex.json | 62 +++++++++++++++++++ .../guiEditor_Main_Final_0e17daca5f3e1.nwd | 2 +- .../guiEditor_Main_Final_nwProject.nwx | 8 +-- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/nw/core/tree.py b/nw/core/tree.py index 786ba89f..85791f42 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -42,21 +42,21 @@ logger = logging.getLogger(__name__) # Translation map for item layouts NOVEL_MAP = { nwItemLayout.SCENE: { - "H1": nwItemLayout.PARTITION, + "H1": nwItemLayout.BOOK, "H2": nwItemLayout.CHAPTER, "H4": nwItemLayout.SCENE, }, nwItemLayout.CHAPTER: { - "H1": nwItemLayout.PARTITION, + "H1": nwItemLayout.BOOK, "H3": nwItemLayout.SCENE, "H4": nwItemLayout.SCENE, }, nwItemLayout.UNNUMBERED: { - "H1": nwItemLayout.PARTITION, + "H1": nwItemLayout.BOOK, "H3": nwItemLayout.SCENE, "H4": nwItemLayout.SCENE, }, - nwItemLayout.PARTITION: { + nwItemLayout.BOOK: { "H2": nwItemLayout.CHAPTER, "H3": nwItemLayout.SCENE, "H4": nwItemLayout.SCENE, diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index 2aa3df3c..993b4e26 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -567,5 +567,67 @@ 259, 3 ] + }, + "firstTitle": { + "7a992350f3eb6": [ + "H1", + "T000001" + ], + "8c58a65414c23": [ + "H0", + "T000000" + ], + "88d59a277361b": [ + "H2", + "T000001" + ], + "db7e733775d4d": [ + "H1", + "T000001" + ], + "fb609cd8319dc": [ + "H2", + "T000001" + ], + "88243afbe5ed8": [ + "H3", + "T000001" + ], + "f96ec11c6a3da": [ + "H3", + "T000001" + ], + "846352075de7d": [ + "H2", + "T000001" + ], + "441420a886d82": [ + "H2", + "T000001" + ], + "eb103bc70c90c": [ + "H3", + "T000001" + ], + "f8c0562e50f1b": [ + "H3", + "T000001" + ], + "47666c91c7ccf": [ + "H3", + "T000001" + ], + "4c4f28287af27": [ + "H1", + "T000001" + ], + "2426c6f0ca922": [ + "H1", + "T000001" + ], + "04468803b92e1": [ + "H1", + "T000001" + ] } } \ No newline at end of file diff --git a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd b/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd index 54fef623..c77c3cd6 100644 --- a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd +++ b/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd @@ -1,6 +1,6 @@ %%~name: New Scene %%~path: 31489056e0916/0e17daca5f3e1 -%%~kind: NOVEL/SCENE +%%~kind: NOVEL/BOOK # Novel ## Chapter diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 21011e03..764c4610 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,11 +1,11 @@ - + New Project 4 - 1 - 11 + 2 + 8 True @@ -83,7 +83,7 @@ NOVEL New True - SCENE + BOOK 466 83 4 From 8e3fa892e1d6d141b1c06e099c0413635434191f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 11:55:36 +0100 Subject: [PATCH 025/104] Modify conditions according to #618 --- nw/core/tree.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nw/core/tree.py b/nw/core/tree.py index 85791f42..f739a940 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -39,12 +39,11 @@ from nw.constants import ( logger = logging.getLogger(__name__) -# Translation map for item layouts -NOVEL_MAP = { +# Layout Translation Map +LAYOUT_MAP = { nwItemLayout.SCENE: { "H1": nwItemLayout.BOOK, "H2": nwItemLayout.CHAPTER, - "H4": nwItemLayout.SCENE, }, nwItemLayout.CHAPTER: { "H1": nwItemLayout.BOOK, @@ -56,7 +55,7 @@ NOVEL_MAP = { "H3": nwItemLayout.SCENE, "H4": nwItemLayout.SCENE, }, - nwItemLayout.BOOK: { + nwItemLayout.PARTITION: { "H2": nwItemLayout.CHAPTER, "H3": nwItemLayout.SCENE, "H4": nwItemLayout.SCENE, @@ -241,9 +240,9 @@ class NWTree(): return False iLayout = tItem.itemLayout - if iLayout in NOVEL_MAP: - if hLevel in NOVEL_MAP[iLayout]: - tItem.itemLayout = NOVEL_MAP[iLayout][hLevel] + if iLayout in LAYOUT_MAP: + if hLevel in LAYOUT_MAP[iLayout]: + tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel] return True return False From 4e7888259f7d2ed5afb0161164d9c5369086f254 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 13:17:31 +0100 Subject: [PATCH 026/104] Add logging wrapper for exceptions --- nw/__init__.py | 4 ++-- nw/error.py | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 02ac7162..2ee82184 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -32,7 +32,7 @@ import logging from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage -from nw.error import exceptionHandler +from nw.error import exceptionHandler, logException from nw.config import Config ## @@ -270,7 +270,7 @@ def main(sysArgs=None): info["CFBundleName"] = "novelWriter" except ImportError as e: logger.error("Failed to set application name") - logger.error(str(e)) + logException(e) # Import GUI (after dependency checks), and launch from nw.guimain import GuiMain diff --git a/nw/error.py b/nw/error.py index 1f879e37..ecfc3b77 100644 --- a/nw/error.py +++ b/nw/error.py @@ -24,12 +24,31 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import sys +import logging + from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel, QDialogButtonBox ) +logger = logging.getLogger(__name__) + +# =============================================================================================== # +# Utility Functions +# =============================================================================================== # + +def logException(exObj): + """Log the content of an exception message. + """ + exType, exValue, _ = sys.exc_info() + logger.error("%s: %s" % (exType.__name__, str(exValue).strip("'"))) + +# =============================================================================================== # +# Error Handler +# =============================================================================================== # + class NWErrorMessage(QDialog): def __init__(self, parent): @@ -72,7 +91,6 @@ class NWErrorMessage(QDialog): """Generate a message and append session data, error info and error traceback. """ - import sys from traceback import format_tb from nw import __issuesurl__, __version__ from PyQt5.Qt import PYQT_VERSION_STR @@ -133,15 +151,12 @@ class NWErrorMessage(QDialog): # END Class NWErrorMessage - def exceptionHandler(exType, exValue, exTrace): """Function to catch unhandled global exceptions. """ - import logging from traceback import print_tb from PyQt5.QtWidgets import qApp - logger = logging.getLogger(__name__) logger.critical("%s: %s" % (exType.__name__, str(exValue))) print_tb(exTrace) From 286efade590289345cef3cca1ab80de3be08e3c4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 13:22:38 +0100 Subject: [PATCH 027/104] Add more check functions --- nw/common.py | 38 ++++++++++-- tests/test_base/test_base_common.py | 92 ++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/nw/common.py b/nw/common.py index 11546c88..826f7370 100644 --- a/nw/common.py +++ b/nw/common.py @@ -30,7 +30,9 @@ from datetime import datetime from PyQt5.QtWidgets import qApp -from nw.constants import nwConst, nwUnicode +from nw.constants import ( + nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout +) logger = logging.getLogger(__name__) @@ -103,11 +105,39 @@ def isHandle(theString): return False if len(theString) != 13: return False - invalidChar = False for c in theString: if c not in "0123456789abcdef": - invalidChar = True - return not invalidChar + return False + return True + +def isTitleTag(theString): + """Check if a string is a valid title string. + """ + if not isinstance(theString, str): + return False + if len(theString) != 7: + return False + if not theString.startswith("T"): + return False + for c in theString[1:]: + if c not in "0123456789": + return False + return True + +def isItemClass(theString): + """Check if an item is a calid nwItemClass identifier. + """ + return theString in nwItemClass.__members__ + +def isItemType(theString): + """Check if an item is a calid nwItemType identifier. + """ + return theString in nwItemType.__members__ + +def isItemLayout(theString): + """Check if an item is a calid nwItemLayout identifier. + """ + return theString in nwItemLayout.__members__ def hexToInt(value, default=0): """Convert a hex string to an integer. diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 79b6562a..4b076a64 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -26,7 +26,8 @@ import pytest from nw.common import ( checkString, checkBool, checkInt, colRange, formatInt, transferCase, fuzzyTime, checkHandle, formatTimeStamp, formatTime, hexToInt, - makeFileNameSafe + makeFileNameSafe, isHandle, isTitleTag, isItemClass, isItemType, + isItemLayout ) from tools import cmpList @@ -88,6 +89,95 @@ def testBaseCommon_CheckHandle(): # END Test testBaseCommon_CheckHandle +@pytest.mark.base +def testBaseCommon_IsHandle(): + """Test the isHandle function. + """ + assert isHandle("47666c91c7ccf") + + assert not isHandle("47666C91C7CCF") + assert not isHandle("h7666c91c7ccf") + assert not isHandle("None") + assert not isHandle(None) + assert not isHandle("STUFF") + +# END Test testBaseCommon_IsHandle + +@pytest.mark.base +def testBaseCommon_IsTitleTag(): + """Test the isItemClass function. + """ + assert isTitleTag("T123456") + + assert not isTitleTag("t123456") + assert not isTitleTag("S123456") + assert not isTitleTag("T12345A") + assert not isTitleTag("T1234567") + + assert not isTitleTag("None") + assert not isTitleTag(None) + assert not isTitleTag("STUFF") + +# END Test testBaseCommon_IsTitleTag + +@pytest.mark.base +def testBaseCommon_IsItemClass(): + """Test the isItemClass function. + """ + assert isItemClass("NO_CLASS") + assert isItemClass("NOVEL") + assert isItemClass("PLOT") + assert isItemClass("CHARACTER") + assert isItemClass("WORLD") + assert isItemClass("TIMELINE") + assert isItemClass("OBJECT") + assert isItemClass("ENTITY") + assert isItemClass("CUSTOM") + assert isItemClass("ARCHIVE") + assert isItemClass("TRASH") + + assert not isItemClass("None") + assert not isItemClass(None) + assert not isItemClass("STUFF") + +# END Test testBaseCommon_IsItemClass + +@pytest.mark.base +def testBaseCommon_IsItemType(): + """Test the isItemType function. + """ + assert isItemType("NO_TYPE") + assert isItemType("ROOT") + assert isItemType("FOLDER") + assert isItemType("FILE") + assert isItemType("TRASH") + + assert not isItemType("None") + assert not isItemType(None) + assert not isItemType("STUFF") + +# END Test testBaseCommon_IsItemType + +@pytest.mark.base +def testBaseCommon_IsItemLayout(): + """Test the isItemLayout function. + """ + assert isItemLayout("NO_LAYOUT") + assert isItemLayout("TITLE") + assert isItemLayout("BOOK") + assert isItemLayout("PAGE") + assert isItemLayout("PARTITION") + assert isItemLayout("UNNUMBERED") + assert isItemLayout("CHAPTER") + assert isItemLayout("SCENE") + assert isItemLayout("NOTE") + + assert not isItemLayout("None") + assert not isItemLayout(None) + assert not isItemLayout("STUFF") + +# END Test testBaseCommon_IsItemLayout + @pytest.mark.base def testBaseCommon_HexToInt(): """Test the hexToInt function. From a2f223a9928aa84205c1093e11064bd711fa4ee0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 18:15:00 +0100 Subject: [PATCH 028/104] Perform a detailed check of the loaded index to make sure everything is in order --- nw/core/index.py | 184 ++++++++-- tests/test_core/test_core_index.py | 533 +++++++++++++++++++++++++++-- 2 files changed, 660 insertions(+), 57 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index ba2e06f3..37cddebd 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -36,11 +36,13 @@ from nw.constants import ( ) from nw.core.document import NWDoc from nw.core.tools import countWords +from nw.common import isHandle, isTitleTag, isItemClass, isItemLayout logger = logging.getLogger(__name__) class NWIndex(): + H_VALID = ("H0", "H1", "H2", "H3", "H4") H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} def __init__(self, theProject, theParent): @@ -155,6 +157,11 @@ class NWIndex(): except Exception as e: logger.error("Failed to load index file") logger.error(str(e)) + self.indexBroken = True + self.theParent.makeAlert( + "Could not load cached index file. Rebuilding index.", + nwAlert.WARN + ) return False self._tagIndex = theData.get("tagIndex", {}) @@ -200,37 +207,25 @@ class NWIndex(): elements it should. """ logger.debug("Checking index") - self.indexBroken = False + tStart = time() try: - for tTag in self._tagIndex: - if len(self._tagIndex[tTag]) != 4: - self.indexBroken = True + self._checkTagIndex() + self._checkRefIndex() + self._checkNovelNoteIndex("novelIndex") + self._checkNovelNoteIndex("noteIndex") + self._checkTextCounts() + self.indexBroken = False - for tHandle in self._refIndex: - for sTitle in self._refIndex[tHandle]: - for tEntry in self._refIndex[tHandle][sTitle]["tags"]: - if len(tEntry) != 3: - self.indexBroken = True - - for tHandle in self._novelIndex: - for sLine in self._novelIndex[tHandle]: - if len(self._novelIndex[tHandle][sLine].keys()) != 8: - self.indexBroken = True - - for tHandle in self._noteIndex: - for sLine in self._noteIndex[tHandle]: - if len(self._noteIndex[tHandle][sLine].keys()) != 8: - self.indexBroken = True - - for tHandle in self._textCounts: - if len(self._textCounts[tHandle]) != 3: - self.indexBroken = True - - except Exception: + except Exception as e: + logger.error("Error while checking index") + nw.logException(e) self.indexBroken = True + tEnd = time() + logger.debug("Index check took %.3f ms" % ((tEnd - tStart)*1000)) logger.debug("Index check complete") + if self.indexBroken: self.clearIndex() self.theParent.makeAlert( @@ -745,4 +740,143 @@ class NWIndex(): return theHandles + ## + # Index Checkers + ## + + def _checkTagIndex(self): + """Scan the tag index for errors. + Waring: This function raises exceptions. + """ + for tTag in self._tagIndex: + if not isinstance(tTag, str): + raise KeyError("tagIndex key is not a string") + + tEntry = self._tagIndex[tTag] + if len(tEntry) != 4: + raise IndexError("tagIndex[a] expected 4 values") + if not isinstance(tEntry[0], int): + raise ValueError("tagIndex[a][0] is not an integer") + if not isHandle(tEntry[1]): + raise ValueError("tagIndex[a][1] is not a handle") + if not isItemClass(tEntry[2]): + raise ValueError("tagIndex[a][2] is not an nwItemClass") + if not isTitleTag(tEntry[3]): + raise ValueError("tagIndex[a][3] is not a title tag") + + return + + def _checkRefIndex(self): + """Scan the reference index for errors. + Waring: This function raises exceptions. + """ + for tHandle in self._refIndex: + if not isHandle(tHandle): + raise KeyError("refIndex key is not a handle") + + hEntry = self._refIndex[tHandle] + for sTitle in hEntry: + if not isTitleTag(sTitle): + raise KeyError("refIndex[a] key is not a title tag") + + sEntry = hEntry[sTitle] + if "tags" not in sEntry: + raise KeyError("refIndex[a][b] has no 'tag' key") + for tEntry in sEntry["tags"]: + if len(tEntry) != 3: + raise IndexError("refIndex[a][b][tags][i] expected 3 values") + if not isinstance(tEntry[0], int): + raise ValueError("refIndex[a][b][tags][i][0] is not an integer") + if not tEntry[1] in nwKeyWords.VALID_KEYS: + raise ValueError("refIndex[a][b][tags][i][1] is not a keyword") + if not isinstance(tEntry[2], str): + raise ValueError("refIndex[a][b][tags][i][2] is not a string") + + if "updated" not in sEntry: + raise KeyError("refIndex[a][b] has no 'updated' key") + if not isinstance(sEntry["updated"], int): + raise ValueError("%refIndex[a][b][updated] is not an integer") + + return + + def _checkNovelNoteIndex(self, idxName): + """Scan the novel or note index for errors. + Waring: This function raises exceptions. + """ + if idxName == "novelIndex": + theIndex = self._novelIndex + elif idxName == "noteIndex": + theIndex = self._noteIndex + else: + raise IndexError("Unknown index %s" % idxName) + + for tHandle in theIndex: + if not isHandle(tHandle): + raise KeyError("%s key is not a handle" % idxName) + + hEntry = theIndex[tHandle] + for sTitle in theIndex[tHandle]: + if not isTitleTag(sTitle): + raise KeyError("%s[a] key is not a title tag" % idxName) + + sEntry = hEntry[sTitle] + if len(sEntry) != 8: + raise IndexError("%s[a][b] expected 8 values" % idxName) + + if "level" not in sEntry: + raise KeyError("%s[a][b] has no 'level' key" % idxName) + if "title" not in sEntry: + raise KeyError("%s[a][b] has no 'title' key" % idxName) + if "layout" not in sEntry: + raise KeyError("%s[a][b] has no 'layout' key" % idxName) + if "synopsis" not in sEntry: + raise KeyError("%s[a][b] has no 'synopsis' key" % idxName) + if "cCount" not in sEntry: + raise KeyError("%s[a][b] has no 'cCount' key" % idxName) + if "wCount" not in sEntry: + raise KeyError("%s[a][b] has no 'wCount' key" % idxName) + if "pCount" not in sEntry: + raise KeyError("%s[a][b] has no 'pCount' key" % idxName) + if "updated" not in sEntry: + raise KeyError("%s[a][b] has no 'updated' key" % idxName) + + if not sEntry["level"] in self.H_VALID: + raise ValueError("%s[a][b][level] is not a header level" % idxName) + if not isinstance(sEntry["title"], str): + raise ValueError("%s[a][b][title] is not a string" % idxName) + if not isItemLayout(sEntry["layout"]): + raise ValueError("%s[a][b][layout] is not an nwItemLayout" % idxName) + if not isinstance(sEntry["synopsis"], str): + raise ValueError("%s[a][b][synopsis] is not a string" % idxName) + if not isinstance(sEntry["cCount"], int): + raise ValueError("%s[a][b][cCount] is not an integer" % idxName) + if not isinstance(sEntry["wCount"], int): + raise ValueError("%s[a][b][wCount] is not an integer" % idxName) + if not isinstance(sEntry["pCount"], int): + raise ValueError("%s[a][b][pCount] is not an integer" % idxName) + if not isinstance(sEntry["updated"], int): + raise ValueError("%s[a][b][updated] is not an integer" % idxName) + + return + + def _checkTextCounts(self): + """Scan the text counts index for errors. + Waring: This function raises exceptions. + """ + for tHandle in self._textCounts: + if not isHandle(tHandle): + raise KeyError("textCounts key is not a handle") + + tEntry = self._textCounts[tHandle] + if len(tEntry) != 3: + raise IndexError("textCounts[a] expected 3 values") + if not isinstance(tEntry[0], int): + raise ValueError("textCounts[a][0] is not an integer") + if not isinstance(tEntry[1], int): + raise ValueError("textCounts[a][1] is not an integer") + if not isinstance(tEntry[2], int): + raise ValueError("textCounts[a][2] is not an integer") + + return + # END Class NWIndex diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index e10000d6..5e1e9223 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -115,38 +115,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir): # Break the index and check that we notice assert not theIndex.indexBroken - theIndex._tagIndex["Bod"].append("Stuff") # No longer len() == 4 - theIndex.checkIndex() - assert theIndex.indexBroken - - assert theIndex.loadIndex() - assert not theIndex.indexBroken - theIndex._refIndex["fb609cd8319dc"]["T000001"]["tags"].append("Stuff") # No longer len() == 3 - theIndex.checkIndex() - assert theIndex.indexBroken - - assert theIndex.loadIndex() - assert not theIndex.indexBroken - theIndex._novelIndex["7a992350f3eb6"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8 - theIndex.checkIndex() - assert theIndex.indexBroken - - assert theIndex.loadIndex() - assert not theIndex.indexBroken - theIndex._noteIndex["4c4f28287af27"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8 - theIndex.checkIndex() - assert theIndex.indexBroken - - assert theIndex.loadIndex() - assert not theIndex.indexBroken - theIndex._textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3 - theIndex.checkIndex() - assert theIndex.indexBroken - - # Make the try/except trigger as well - assert theIndex.loadIndex() - assert not theIndex.indexBroken - theIndex._refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name + theIndex._tagIndex["Bod"].append("Stuff") theIndex.checkIndex() assert theIndex.indexBroken @@ -676,3 +645,503 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert theProject.closeProject() # END Test testCoreIndex_ExtractData + +@pytest.mark.core +def testCoreIndex_CheckTagIndex(dummyGUI): + """Test the tag index checker. + """ + theProject = NWProject(dummyGUI) + theIndex = NWIndex(theProject, dummyGUI) + + # Valid Index + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], + } + assert theIndex._checkTagIndex() is None + + # Wrong Key Type + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + 123456: [3, "bb2c23b3c42cc", "CHARACTER", "T000001"], + } + with pytest.raises(KeyError): + theIndex._checkTagIndex() + + # Wrong Length + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001", "Stuff"], + } + with pytest.raises(IndexError): + theIndex._checkTagIndex() + + # Wrong Type of Entry 0 + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + "Jane": ["3", "bb2c23b3c42cc", "CHARACTER", "T000001"], + } + with pytest.raises(ValueError): + theIndex._checkTagIndex() + + # Wrong Type of Entry 1 + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + "Jane": [3, 0xbb2c23b3c42cc, "CHARACTER", "T000001"], + } + with pytest.raises(ValueError): + theIndex._checkTagIndex() + + # Wrong Type of Entry 2 + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + "Jane": [3, "bb2c23b3c42cc", "INVALID_CLASS", "T000001"], + } + with pytest.raises(ValueError): + theIndex._checkTagIndex() + + # Wrong Type of Entry 3 + theIndex._tagIndex = { + "John": [3, "14298de4d9524", "CHARACTER", "T000001"], + "Jane": [3, "bb2c23b3c42cc", "CHARACTER", "INVALID"], + } + with pytest.raises(ValueError): + theIndex._checkTagIndex() + +# END Test testCoreIndex_CheckTagIndex + +@pytest.mark.core +def testCoreIndex_CheckRefIndex(dummyGUI): + """Test the reference index checker. + """ + theProject = NWProject(dummyGUI) + theIndex = NWIndex(theProject, dummyGUI) + + # Valid Index + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [ + [3, "@pov", "Jane"], [4, "@location", "Earth"] + ], "updated": 1611922868} + } + } + assert theIndex._checkRefIndex() is None + + # Invalid Handle + theIndex._refIndex = { + "Ha2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [ + [3, "@pov", "Jane"], [4, "@location", "Earth"] + ], "updated": 1611922868} + } + } + with pytest.raises(KeyError): + theIndex._checkRefIndex() + + # Invalid Title + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "INVALID": {"tags": [ + [3, "@pov", "Jane"], [4, "@location", "Earth"] + ], "updated": 1611922868} + } + } + with pytest.raises(KeyError): + theIndex._checkRefIndex() + + # Missing 'tags' + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"updated": 1611922868} + } + } + with pytest.raises(KeyError): + theIndex._checkRefIndex() + + # Wrong Length of 'tags' + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [ + [3, "@pov", "Jane"], [4, "@location", "Earth", "Stuff"] + ], "updated": 1611922868} + } + } + with pytest.raises(IndexError): + theIndex._checkRefIndex() + + # Wrong Type of 'tags' Entry 0 + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [ + [3, "@pov", "Jane"], ["4", "@location", "Earth"] + ], "updated": 1611922868} + } + } + with pytest.raises(ValueError): + theIndex._checkRefIndex() + + # Wrong Type of 'tags' Entry 1 + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [ + [3, "@pov", "Jane"], [4, "@stuff", "Earth"] + ], "updated": 1611922868} + } + } + with pytest.raises(ValueError): + theIndex._checkRefIndex() + + # Wrong Type of 'tags' Entry 1 + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [ + [3, "@pov", "Jane"], [4, "@location", 123456] + ], "updated": 1611922868} + } + } + with pytest.raises(ValueError): + theIndex._checkRefIndex() + + # Missing 'updated' + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": []} + } + } + with pytest.raises(KeyError): + theIndex._checkRefIndex() + + # Wrong Type of 'updated' Entry 1 + theIndex._refIndex = { + "6a2d6d5f4f401": { + "T000000": {"tags": [], "updated": 1611922868}, + "T000001": {"tags": [], "updated": "1611922868"} + } + } + with pytest.raises(ValueError): + theIndex._checkRefIndex() + +# END Test testCoreIndex_CheckRefIndex + +@pytest.mark.core +def testCoreIndex_CheckNovelNoteIndex(dummyGUI): + """Test the novel and note index checkers. + """ + theProject = NWProject(dummyGUI) + theIndex = NWIndex(theProject, dummyGUI) + + # Valid Index + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + theIndex._noteIndex = theIndex._novelIndex.copy() + assert theIndex._checkNovelNoteIndex("novelIndex") is None + assert theIndex._checkNovelNoteIndex("noteIndex") is None + with pytest.raises(IndexError): + theIndex._checkNovelNoteIndex("notAnIndex") + + # Invalid Handle + theIndex._novelIndex = { + "H3b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Invalid Title + theIndex._novelIndex = { + "53b69b83cdafc": { + "INVALID": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Length + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868, "stuff": None + } + } + } + with pytest.raises(IndexError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing Keys + # ============ + + # Missing 'level' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "stuff": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'title' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "stuff": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'layout' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "stuff": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'synopsis' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "stuff": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'cCount' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "stuff": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'wCount' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "stuff": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'pCount' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "stuff": 2, "updated": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Missing 'updated' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "stuff": 1611922868 + } + } + } + with pytest.raises(KeyError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Types + # =========== + + # Wrong Type for 'level' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "XX", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'title' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": 12345678, "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'layout' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "INVALID", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'synopsis' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": 123456, + "cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'cCount' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": "72", "wCount": 15, "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'wCount' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": "15", "pCount": 2, "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'pCount' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": "2", "updated": 1611922868 + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + + # Wrong Type for 'updated' + theIndex._novelIndex = { + "53b69b83cdafc": { + "T000001": { + "level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text", + "cCount": 72, "wCount": 15, "pCount": 2, "updated": "1611922868" + } + } + } + with pytest.raises(ValueError): + theIndex._checkNovelNoteIndex("novelIndex") + +# END Test testCoreIndex_CheckNovelNoteIndex + +@pytest.mark.core +def testCoreIndex_CheckTextCounts(dummyGUI): + """Test the text counts checker. + """ + theProject = NWProject(dummyGUI) + theIndex = NWIndex(theProject, dummyGUI) + + # Valid Index + theIndex._textCounts = { + "53b69b83cdafc": [72, 15, 2], + "974e400180a99": [210, 40, 2], + } + assert theIndex._checkTextCounts() is None + + # Invalid Handle + theIndex._textCounts = { + "53b69b83cdafc": [72, 15, 2], + "h74e400180a99": [210, 40, 2], + } + with pytest.raises(KeyError): + theIndex._checkTextCounts() + + # Wrong Length + theIndex._textCounts = { + "53b69b83cdafc": [72, 15, 2], + "974e400180a99": [210, 40, 2, 8], + } + with pytest.raises(IndexError): + theIndex._checkTextCounts() + + # Type of Entry 0 + theIndex._textCounts = { + "53b69b83cdafc": [72, 15, 2], + "974e400180a99": ["210", 40, 2], + } + with pytest.raises(ValueError): + theIndex._checkTextCounts() + + # Type of Entry 1 + theIndex._textCounts = { + "53b69b83cdafc": [72, 15, 2], + "974e400180a99": [210, "40", 2], + } + with pytest.raises(ValueError): + theIndex._checkTextCounts() + + # Type of Entry 2 + theIndex._textCounts = { + "53b69b83cdafc": [72, 15, 2], + "974e400180a99": [210, 40, "2"], + } + with pytest.raises(ValueError): + theIndex._checkTextCounts() + +# END Test testCoreIndex_CheckTextCounts From 89ba4aeeaf413c5aa7a5e8a6b8d0f81ebff05581 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 18:29:50 +0100 Subject: [PATCH 029/104] Use the logException function in all places where exception messages are written to the error log --- nw/__init__.py | 4 ++-- nw/config.py | 13 +++++++------ nw/core/index.py | 12 ++++++------ nw/core/options.py | 9 +++++---- nw/core/project.py | 41 +++++++++++++++++++++++------------------ nw/core/spellcheck.py | 16 ++++++++-------- nw/core/tree.py | 6 ++++-- nw/error.py | 2 +- nw/gui/build.py | 12 ++++++------ nw/gui/docviewer.py | 4 ++-- nw/gui/theme.py | 16 ++++++++-------- 11 files changed, 72 insertions(+), 63 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 2ee82184..8cb87d08 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -268,9 +268,9 @@ def main(sysArgs=None): bundle = NSBundle.mainBundle() info = bundle.localizedInfoDictionary() or bundle.infoDictionary() info["CFBundleName"] = "novelWriter" - except ImportError as e: + except ImportError: logger.error("Failed to set application name") - logException(e) + logException() # Import GUI (after dependency checks), and launch from nw.guimain import GuiMain diff --git a/nw/config.py b/nw/config.py index d35582fb..1a9aade2 100644 --- a/nw/config.py +++ b/nw/config.py @@ -38,6 +38,7 @@ from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo from nw.constants import nwConst, nwFiles, nwUnicode from nw.common import splitVersionNumber, formatTimeStamp +from nw.error import logException logger = logging.getLogger(__name__) @@ -293,7 +294,7 @@ class Config: os.mkdir(self.confPath) except Exception as e: logger.error("Could not create folder: %s" % self.confPath) - logger.error(str(e)) + logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.confPath) self.errData.append(str(e)) @@ -316,7 +317,7 @@ class Config: os.mkdir(self.dataPath) except Exception as e: logger.error("Could not create folder: %s" % self.dataPath) - logger.error(str(e)) + logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.dataPath) self.errData.append(str(e)) @@ -361,7 +362,7 @@ class Config: cnfParse.read_file(inFile) except Exception as e: logger.error("Could not load config file") - logger.error(str(e)) + logException() self.hasError = True self.errData.append("Could not load config file") self.errData.append(str(e)) @@ -702,7 +703,7 @@ class Config: self.confChanged = False except Exception as e: logger.error("Could not save config file") - logger.error(str(e)) + logException() self.hasError = True self.errData.append("Could not save config file") self.errData.append(str(e)) @@ -978,9 +979,9 @@ class Config: return self._unpackList( cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST ) - except ValueError as e: + except ValueError: logger.error("Failed to load value from config file.") - logger.error(str(e)) + logException() return cnfDefault return cnfDefault diff --git a/nw/core/index.py b/nw/core/index.py index 37cddebd..7a9c88bc 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -154,9 +154,9 @@ class NWIndex(): try: with open(indexFile, mode="r", encoding="utf8") as inFile: theData = json.load(inFile) - except Exception as e: + except Exception: logger.error("Failed to load index file") - logger.error(str(e)) + nw.logException() self.indexBroken = True self.theParent.makeAlert( "Could not load cached index file. Rebuilding index.", @@ -195,9 +195,9 @@ class NWIndex(): "noteIndex" : self._noteIndex, "textCounts" : self._textCounts, }, outFile, indent=2) - except Exception as e: + except Exception: logger.error("Failed to save index file") - logger.error(str(e)) + nw.logException() return False return True @@ -217,9 +217,9 @@ class NWIndex(): self._checkTextCounts() self.indexBroken = False - except Exception as e: + except Exception: logger.error("Error while checking index") - nw.logException(e) + nw.logException() self.indexBroken = True tEnd = time() diff --git a/nw/core/options.py b/nw/core/options.py index 36da3f32..c0df1855 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -25,6 +25,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import nw import logging import json import os @@ -121,9 +122,9 @@ class OptionState(): try: with open(stateFile, mode="r", encoding="utf8") as inFile: theState = json.load(inFile) - except Exception as e: + except Exception: logger.error("Failed to load GUI options file") - logger.error(str(e)) + nw.logException() return False # Filter out unused variables @@ -148,9 +149,9 @@ class OptionState(): try: with open(stateFile, mode="w+", encoding="utf8") as outFile: json.dump(self.theState, outFile, indent=2) - except Exception as e: + except Exception: logger.error("Failed to save GUI options file") - logger.error(str(e)) + nw.logException() return False return True diff --git a/nw/core/project.py b/nw/core/project.py index a22f7aa9..90d3649f 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1219,9 +1219,9 @@ class NWProject(): if len(theLines) != 4: return ["ERROR"] - except Exception as e: + except Exception: logger.error("Failed to read project lockfile") - logger.error(str(e)) + nw.logException() return ["ERROR"] return theLines @@ -1240,9 +1240,9 @@ class NWProject(): outFile.write("%s\n" % self.mainConf.kernelVer) outFile.write("%d\n" % time()) - except Exception as e: + except Exception: logger.error("Failed to write project lockfile") - logger.error(str(e)) + nw.logException() return False return True @@ -1257,9 +1257,9 @@ class NWProject(): if os.path.isfile(lockFile): try: os.unlink(lockFile) - except Exception as e: + except Exception: logger.error("Failed to remove project lockfile") - logger.error(str(e)) + nw.logException() return False return True @@ -1415,9 +1415,9 @@ class NWProject(): self.notesWCount, )) - except Exception as e: + except Exception: logger.error("Failed to write session stats file") - logger.error(str(e)) + nw.logException() return False return True @@ -1453,17 +1453,19 @@ class NWProject(): os.rename(theFile, newPath) logger.info("Moved file: %s" % theFile) logger.info("New location: %s" % newPath) - except Exception as e: - logger.error(str(e)) + except Exception: errList.append("Could not move: %s" % theFile) + logger.error("Could not move: %s" % theFile) + nw.logException() elif len(dataItem) == 21 and dataItem.endswith("_main.bak"): try: os.unlink(theFile) logger.info("Deleted file: %s" % theFile) - except Exception as e: - logger.error(str(e)) + except Exception: errList.append("Could not delete: %s" % theFile) + logger.error("Could not delete: %s" % theFile) + nw.logException() else: theErr = self._moveUnknownItem(theData, dataItem) @@ -1475,9 +1477,10 @@ class NWProject(): try: os.rmdir(theData) logger.info("Removed folder: %s" % theFolder) - except Exception as e: - logger.error(str(e)) + except Exception: errList.append("Failed to remove: %s" % theFolder) + logger.error("Failed to remove: %s" % theFolder) + nw.logException() return errList @@ -1495,8 +1498,9 @@ class NWProject(): try: os.rename(theSrc, theDst) logger.info("Moved to junk: %s" % theSrc) - except Exception as e: - logger.error(str(e)) + except Exception: + logger.error("Could not move item %s to junk." % theSrc) + nw.logException() return "Could not move item %s to junk." % theSrc return "" @@ -1529,8 +1533,9 @@ class NWProject(): logger.info("Deleting: %s" % rmFile) try: os.unlink(rmFile) - except Exception as e: - logger.error(str(e)) + except Exception: + logger.error("Could not delete: %s" % rmFile) + nw.logException() return False return True diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 8b3807fa..8dd33bbe 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -72,9 +72,9 @@ class NWSpellCheck(): with open(self.projectDict, mode="a+", encoding="utf-8") as outFile: outFile.write("%s\n" % newWord) self.projDict.append(newWord) - except Exception as e: + except Exception: logger.error("Failed to add word to project word list %s" % str(self.projectDict)) - logger.error(str(e)) + nw.logException() return False return True return False @@ -123,9 +123,9 @@ class NWSpellCheck(): if len(theLine) > 0 and theLine not in self.projDict: self.projDict.append(theLine) logger.debug("Project word list contains %d words" % len(self.projDict)) - except Exception as e: + except Exception: logger.error("Failed to load project word list") - logger.error(str(e)) + nw.logException() return False return True @@ -201,9 +201,9 @@ class NWSpellEnchant(NWSpellCheck): try: spTag = self.theDict.tag spName = self.theDict.provider.name - except Exception as e: + except Exception: logger.error("Failed to extract information about the dictionary") - logger.error(str(e)) + nw.logException() spTag = "" spName = "" @@ -261,9 +261,9 @@ class NWSpellSimple(NWSpellCheck): logger.debug("Spell check word list for language %s loaded" % theLang) logger.debug("Word list contains %d words" % len(self.WORDS)) self.spellLanguage = theLang - except Exception as e: + except Exception: logger.error("Failed to load spell check word list for language %s" % theLang) - logger.error(str(e)) + nw.logException() self.spellLanguage = None self._readProjectDictionary(projectDict) diff --git a/nw/core/tree.py b/nw/core/tree.py index 36a56e5c..94e9b97a 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -24,6 +24,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import nw import logging import os @@ -179,8 +180,9 @@ class NWTree(): outFile.write("\n".join(tocList)) outFile.write("\n") - except Exception as e: - logger.error(str(e)) + except Exception: + logger.error("Could not write ToC file") + nw.logException() return False return True diff --git a/nw/error.py b/nw/error.py index ecfc3b77..998b7347 100644 --- a/nw/error.py +++ b/nw/error.py @@ -39,7 +39,7 @@ logger = logging.getLogger(__name__) # Utility Functions # =============================================================================================== # -def logException(exObj): +def logException(): """Log the content of an exception message. """ exType, exValue, _ = sys.exc_info() diff --git a/nw/gui/build.py b/nw/gui/build.py index 5bcea455..18bb4dc7 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -671,9 +671,9 @@ class GuiBuildNovel(QDialog): bldObj.doConvert() bldObj.doPostProcessing() - except Exception as e: + except Exception: logger.error("Failed to generate html of document '%s'" % tItem.itemHandle) - logger.error(str(e)) + nw.logException() if isPreview: self.docView.setText(( "Failed to generate preview. " @@ -997,9 +997,9 @@ class GuiBuildNovel(QDialog): with open(buildCache, mode="r", encoding="utf8") as inFile: theJson = inFile.read() theData = json.loads(theJson) - except Exception as e: + except Exception: logger.error("Failed to load build cache") - logger.error(str(e)) + nw.logException() return False if "htmlText" in theData.keys(): @@ -1026,9 +1026,9 @@ class GuiBuildNovel(QDialog): "htmlStyle" : self.htmlStyle, "buildTime" : self.buildTime, }, indent=2)) - except Exception as e: + except Exception: logger.error("Failed to save build cache") - logger.error(str(e)) + nw.logException() return False return True diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 55e458bb..24d53019 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -180,9 +180,9 @@ class GuiDocViewer(QTextBrowser): aDoc.tokenizeText() aDoc.doConvert() aDoc.doPostProcessing() - except Exception as e: + except Exception: logger.error("Failed to generate preview for document with handle '%s'" % tHandle) - logger.error(str(e)) + nw.logException() self.setText("An error occurred while generating the preview.") return False diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 8aa0c1fe..73d8a7b0 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -265,9 +265,9 @@ class GuiTheme: if os.path.isfile(self.cssFile): with open(self.cssFile, mode="r", encoding="utf8") as inFile: cssData = inFile.read() - except Exception as e: + except Exception: logger.error("Could not load theme css file") - logger.error(str(e)) + nw.logException() return False # Config File @@ -275,9 +275,9 @@ class GuiTheme: try: with open(self.confFile, mode="r", encoding="utf8") as inFile: confParser.read_file(inFile) - except Exception as e: + except Exception: logger.error("Could not load theme settings from: %s" % self.confFile) - logger.error(str(e)) + nw.logException() return False ## Main @@ -333,9 +333,9 @@ class GuiTheme: try: with open(self.syntaxFile, mode="r", encoding="utf8") as inFile: confParser.read_file(inFile) - except Exception as e: + except Exception: logger.error("Could not load syntax colours from: %s" % self.syntaxFile) - logger.error(str(e)) + nw.logException() return False ## Main @@ -637,9 +637,9 @@ class GuiIcons: try: with open(self.confFile, mode="r", encoding="utf8") as inFile: confParser.read_file(inFile) - except Exception as e: + except Exception: logger.error("Could not load icon theme settings from: %s" % self.confFile) - logger.error(str(e)) + nw.logException() return False ## Main From d8cc091f9438199c2d26a24c4cbe3cb73148ea78 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 18:39:48 +0100 Subject: [PATCH 030/104] Also use the new nwItem checkers in the NWItem class --- nw/core/item.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nw/core/item.py b/nw/core/item.py index e2a5a4e8..62460e7e 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -28,8 +28,10 @@ import logging from lxml import etree -from nw.common import checkInt, isHandle from nw.constants import nwItemType, nwItemClass, nwItemLayout +from nw.common import ( + checkInt, isHandle, isItemClass, isItemLayout, isItemType +) logger = logging.getLogger(__name__) @@ -201,7 +203,7 @@ class NWItem(): """ if isinstance(theType, nwItemType): self.itemType = theType - elif theType in nwItemType.__members__: + elif isItemType(theType): self.itemType = nwItemType[theType] else: logger.error("Unrecognised item type '%s'" % theType) @@ -214,7 +216,7 @@ class NWItem(): """ if isinstance(theClass, nwItemClass): self.itemClass = theClass - elif theClass in nwItemClass.__members__: + elif isItemClass(theClass): self.itemClass = nwItemClass[theClass] else: logger.error("Unrecognised item class '%s'" % theClass) @@ -227,7 +229,7 @@ class NWItem(): """ if isinstance(theLayout, nwItemLayout): self.itemLayout = theLayout - elif theLayout in nwItemLayout.__members__: + elif isItemLayout(theLayout): self.itemLayout = nwItemLayout[theLayout] else: logger.error("Unrecognised item layout '%s'" % theLayout) From b7766946d801dbd64461a1322c4c3eadbe9181a4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 Jan 2021 20:43:04 +0100 Subject: [PATCH 031/104] Update index check and test --- nw/core/index.py | 23 +++++++-- tests/test_core/test_core_index.py | 83 ++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 15b4c955..85ec8791 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -220,12 +220,9 @@ class NWIndex(): self._checkNovelNoteIndex("novelIndex") self._checkNovelNoteIndex("noteIndex") self._checkTextCounts() + self._checkFirstTitles() self.indexBroken = False - for tHandle in self._firstTitle: - if len(self._firstTitle[tHandle]) != 2: - self.indexBroken = True - except Exception: logger.error("Error while checking index") nw.logException() @@ -897,4 +894,22 @@ class NWIndex(): return + def _checkFirstTitles(self): + """Scan the first titles index for errors. + Waring: This function raises exceptions. + """ + for tHandle in self._firstTitle: + if not isHandle(tHandle): + raise KeyError("firstTitle key is not a handle") + + tEntry = self._firstTitle[tHandle] + if len(tEntry) != 2: + raise IndexError("firstTitle[a] expected 2 values") + if not tEntry[0] in self.H_VALID: + raise ValueError("firstTitle[a][0] is not a header level") + if not isTitleTag(tEntry[1]): + raise ValueError("firstTitle[a][1] is not a title tag") + + return + # END Class NWIndex diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 5e1e9223..bd2f6b3f 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -492,9 +492,8 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert wC == 12 # Words in text and title only assert pC == 2 # Paragraphs in text only - ## - # getReferences - ## + # getReferences + # ============= # Look up an ivalid handle theRefs = theIndex.getReferences("Not a handle") @@ -506,9 +505,8 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert theRefs["@pov"] == ["Jane"] assert theRefs["@char"] == ["Jane"] - ## - # getBackReferenceList - ## + # getBackReferenceList + # ==================== # None handle should return an empty dict assert theIndex.getBackReferenceList(None) == {} @@ -517,16 +515,15 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): theRefs = theIndex.getBackReferenceList(cHandle) assert theRefs == {nHandle: "T000001"} - ## - # getTagSource - ## + # getTagSource + # ============ assert theIndex.getTagSource("Jane") == (cHandle, 2, "T000001") assert theIndex.getTagSource("John") == (None, 0, "T000000") - ## - # getCounts for whole text and sections - ## + # getCounts + # ========= + # For whole text and sections # Get section counts for a novel file assert theIndex.scanText(nHandle, ( @@ -555,7 +552,7 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert wC == 12 assert pC == 2 - # First part + # Second part cC, wC, pC = theIndex.getCounts(nHandle, "T000011") assert cC == 62 assert wC == 12 @@ -588,15 +585,19 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert wC == 12 assert pC == 2 - # First part + # Second part cC, wC, pC = theIndex.getCounts(cHandle, "T000011") assert cC == 62 assert wC == 12 assert pC == 2 - ## - # Novel Stats - ## + # getFirstTitle + # ============= + + assert theIndex.getFirstTitle(cHandle) == ["H1", "T000001"] + + # Novel Stats + # =========== hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c") sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c") @@ -1145,3 +1146,51 @@ def testCoreIndex_CheckTextCounts(dummyGUI): theIndex._checkTextCounts() # END Test testCoreIndex_CheckTextCounts + +@pytest.mark.core +def testCoreIndex_CheckFirstTitle(dummyGUI): + """Test the first title checker. + """ + theProject = NWProject(dummyGUI) + theIndex = NWIndex(theProject, dummyGUI) + + # Valid Index + theIndex._firstTitle = { + "53b69b83cdafc": ["H1", "T000001"], + "974e400180a99": ["H0", "T000000"], + } + assert theIndex._checkFirstTitles() is None + + # Invalid Handle + theIndex._firstTitle = { + "53b69b83cdafc": ["H1", "T000001"], + "h74e400180a99": ["H0", "T000000"], + } + with pytest.raises(KeyError): + theIndex._checkFirstTitles() + + # Wrong Length + theIndex._firstTitle = { + "53b69b83cdafc": ["H1", "T000001"], + "974e400180a99": ["H0", "T000000", "stuff"], + } + with pytest.raises(IndexError): + theIndex._checkFirstTitles() + + # Wrong Header + theIndex._firstTitle = { + "53b69b83cdafc": ["H1", "T000001"], + "974e400180a99": ["XX", "T000000"], + } + with pytest.raises(ValueError): + theIndex._checkFirstTitles() + + # Wrong Title + theIndex._firstTitle = { + "53b69b83cdafc": ["H1", "T000001"], + "974e400180a99": ["H0", "INVALID"], + } + with pytest.raises(ValueError): + theIndex._checkFirstTitles() + +# END Test testCoreIndex_CheckFirstTitle From cab1530afc775f8a96cea2db281e35144d1e0bcf Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 01:03:54 +0100 Subject: [PATCH 032/104] Add debug log output for layout change --- nw/core/tree.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nw/core/tree.py b/nw/core/tree.py index dd72c3ff..5fdc55ba 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -245,6 +245,9 @@ class NWTree(): if iLayout in LAYOUT_MAP: if hLevel in LAYOUT_MAP[iLayout]: tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel] + logger.debug("Changed layout for %s from %s to %s" % ( + tHandle, iLayout.name, tItem.itemLayout.name + )) return True return False From 0624f9103f229fc940ce0a3a53e1db61616248ba Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 01:28:05 +0100 Subject: [PATCH 033/104] Get test coverage of NWTree back to 100% --- tests/test_core/test_core_tree.py | 153 +++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 14 deletions(-) diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 62803830..d4588b37 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -123,9 +123,9 @@ def testCoreTree_BuildTree(dummyGUI, dummyItems): assert not theTree.isTrashRoot("a000000000003") aHandles = [] - for tHandle, pHande, nwItem in dummyItems: + for tHandle, pHandle, nwItem in dummyItems: aHandles.append(tHandle) - assert theTree.append(tHandle, pHande, nwItem) + assert theTree.append(tHandle, pHandle, nwItem) assert theTree._treeChanged @@ -202,13 +202,13 @@ def testCoreTree_BuildTree(dummyGUI, dummyItems): @pytest.mark.core def testCoreTree_Methods(dummyGUI, dummyItems): - """Test building a project tree from a list of items. + """Test bvarious class methods. """ theProject = NWProject(dummyGUI) theTree = NWTree(theProject) - for tHandle, pHande, nwItem in dummyItems: - theTree.append(tHandle, pHande, nwItem) + for tHandle, pHandle, nwItem in dummyItems: + theTree.append(tHandle, pHandle, nwItem) assert len(theTree) == len(dummyItems) @@ -256,6 +256,131 @@ def testCoreTree_Methods(dummyGUI, dummyItems): # END Test testCoreTree_Methods +@pytest.mark.core +def testCoreTree_UpdateItemLayout(dummyGUI, dummyItems): + """Test building a project tree from a list of items. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + for tHandle, pHandle, nwItem in dummyItems: + theTree.append(tHandle, pHandle, nwItem) + + assert len(theTree) == len(dummyItems) + + # Check rejected items + assert not theTree.updateItemLayout("0000000000000", "H1") # Non-existent handle + assert not theTree.updateItemLayout("a000000000004", "H2") # Character file + assert not theTree.updateItemLayout("c000000000002", "H0") # Wrong header level + + cHandle = "c000000000002" + + # Check layouts we won't change + theTree[cHandle].setLayout(nwItemLayout.NO_LAYOUT) + assert not theTree.updateItemLayout("c000000000002", "H1") + + theTree[cHandle].setLayout(nwItemLayout.TITLE) + assert not theTree.updateItemLayout("c000000000002", "H1") + + theTree[cHandle].setLayout(nwItemLayout.PAGE) + assert not theTree.updateItemLayout("c000000000002", "H1") + + theTree[cHandle].setLayout(nwItemLayout.NOTE) + assert not theTree.updateItemLayout("c000000000002", "H1") + + # BOOK is also a layout we change to, but never from + theTree[cHandle].setLayout(nwItemLayout.BOOK) + assert not theTree.updateItemLayout("c000000000002", "H1") + + # Test SCENE Changes + # ================== + + # H1 -> BOOK + theTree[cHandle].setLayout(nwItemLayout.SCENE) + assert theTree.updateItemLayout("c000000000002", "H1") + assert theTree[cHandle].itemLayout == nwItemLayout.BOOK + + # H2 -> CHAPTER + theTree[cHandle].setLayout(nwItemLayout.SCENE) + assert theTree.updateItemLayout("c000000000002", "H2") + assert theTree[cHandle].itemLayout == nwItemLayout.CHAPTER + + # H3 -> No CHange + theTree[cHandle].setLayout(nwItemLayout.SCENE) + assert not theTree.updateItemLayout("c000000000002", "H3") + + # H4 -> No CHange + theTree[cHandle].setLayout(nwItemLayout.SCENE) + assert not theTree.updateItemLayout("c000000000002", "H4") + + # Test CHAPTER Changes + # ==================== + + # H1 -> BOOK + theTree[cHandle].setLayout(nwItemLayout.CHAPTER) + assert theTree.updateItemLayout("c000000000002", "H1") + assert theTree[cHandle].itemLayout == nwItemLayout.BOOK + + # H2 -> No Change + theTree[cHandle].setLayout(nwItemLayout.CHAPTER) + assert not theTree.updateItemLayout("c000000000002", "H2") + + # H3 -> SCENE + theTree[cHandle].setLayout(nwItemLayout.CHAPTER) + assert theTree.updateItemLayout("c000000000002", "H3") + assert theTree[cHandle].itemLayout == nwItemLayout.SCENE + + # H4 -> SCENE + theTree[cHandle].setLayout(nwItemLayout.CHAPTER) + assert theTree.updateItemLayout("c000000000002", "H4") + assert theTree[cHandle].itemLayout == nwItemLayout.SCENE + + # Test UNNUMBERED Changes + # ======================= + + # H1 -> BOOK + theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED) + assert theTree.updateItemLayout("c000000000002", "H1") + assert theTree[cHandle].itemLayout == nwItemLayout.BOOK + + # H2 -> No Change + theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED) + assert not theTree.updateItemLayout("c000000000002", "H2") + + # H3 -> SCENE + theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED) + assert theTree.updateItemLayout("c000000000002", "H3") + assert theTree[cHandle].itemLayout == nwItemLayout.SCENE + + # H4 -> SCENE + theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED) + assert theTree.updateItemLayout("c000000000002", "H4") + assert theTree[cHandle].itemLayout == nwItemLayout.SCENE + + # Test PARTITION Changes + # ====================== + + # H1 -> BOOK + theTree[cHandle].setLayout(nwItemLayout.PARTITION) + assert not theTree.updateItemLayout("c000000000002", "H1") + + # H2 -> No Change + theTree[cHandle].setLayout(nwItemLayout.PARTITION) + assert theTree.updateItemLayout("c000000000002", "H2") + assert theTree[cHandle].itemLayout == nwItemLayout.CHAPTER + + # H3 -> SCENE + theTree[cHandle].setLayout(nwItemLayout.PARTITION) + assert theTree.updateItemLayout("c000000000002", "H3") + assert theTree[cHandle].itemLayout == nwItemLayout.SCENE + + # H4 -> SCENE + theTree[cHandle].setLayout(nwItemLayout.PARTITION) + assert theTree.updateItemLayout("c000000000002", "H4") + assert theTree[cHandle].itemLayout == nwItemLayout.SCENE + +# END Test testCoreTree_UpdateItemLayout + @pytest.mark.core def testCoreTree_MakeHandles(monkeypatch, dummyGUI): """Test generating item handles. @@ -296,8 +421,8 @@ def testCoreTree_Stats(dummyGUI, dummyItems): theProject = NWProject(dummyGUI) theTree = NWTree(theProject) - for tHandle, pHande, nwItem in dummyItems: - theTree.append(tHandle, pHande, nwItem) + for tHandle, pHandle, nwItem in dummyItems: + theTree.append(tHandle, pHandle, nwItem) assert len(theTree) == len(dummyItems) theTree._treeOrder.append("dummy") @@ -323,9 +448,9 @@ def testCoreTree_Reorder(dummyGUI, dummyItems): theTree = NWTree(theProject) aHandle = [] - for tHandle, pHande, nwItem in dummyItems: + for tHandle, pHandle, nwItem in dummyItems: aHandle.append(tHandle) - theTree.append(tHandle, pHande, nwItem) + theTree.append(tHandle, pHandle, nwItem) assert len(theTree) == len(dummyItems) @@ -348,13 +473,13 @@ def testCoreTree_Reorder(dummyGUI, dummyItems): @pytest.mark.core def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems): - """Test changing tree order. + """Test packing and unpacking the tree to and from XML. """ theProject = NWProject(dummyGUI) theTree = NWTree(theProject) - for tHandle, pHande, nwItem in dummyItems: - theTree.append(tHandle, pHande, nwItem) + for tHandle, pHandle, nwItem in dummyItems: + theTree.append(tHandle, pHandle, nwItem) assert len(theTree) == len(dummyItems) @@ -408,8 +533,8 @@ def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir): theProject = NWProject(dummyGUI) theTree = NWTree(theProject) - for tHandle, pHande, nwItem in dummyItems: - theTree.append(tHandle, pHande, nwItem) + for tHandle, pHandle, nwItem in dummyItems: + theTree.append(tHandle, pHandle, nwItem) assert len(theTree) == len(dummyItems) theTree._treeOrder.append("dummy") From 087735a08ffd17cd96e2afe20c9128ec59cbba40 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 18:54:13 +0100 Subject: [PATCH 034/104] Add timestamp to document meta data --- nw/core/document.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nw/core/document.py b/nw/core/document.py index 7e833854..42db1603 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -27,8 +27,10 @@ along with this program. If not, see . import logging import os +from time import time + from nw.constants import nwAlert -from nw.common import isHandle +from nw.common import isHandle, formatTimeStamp from nw.constants import nwItemLayout, nwItemClass logger = logging.getLogger(__name__) @@ -151,6 +153,7 @@ class NWDoc(): f"%%~name: {self._theItem.itemName}\n" f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n" f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n" + f"%%~time: {formatTimeStamp(time())}\n" ) try: From 7a3c33fde3871a13ab54602845806f6088511865 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 18:54:30 +0100 Subject: [PATCH 035/104] Fix tests --- sample/content/636b6aa9b697b.nwd | 1 + .../guiEditor_Main_Final_031b4af5197ec.nwd | 1 + .../guiEditor_Main_Final_0e17daca5f3e1.nwd | 1 + .../guiEditor_Main_Final_1a6562590ef19.nwd | 1 + .../guiEditor_Main_Final_41cfc0d1f2d12.nwd | 1 + tests/reference/guiMerge_73475cb40a568.nwd | 1 + tests/reference/guiSplit_031b4af5197ec.nwd | 1 + tests/reference/guiSplit_25fc0e7096fc6.nwd | 1 + tests/reference/guiSplit_2858dcd1057d3.nwd | 1 + tests/reference/guiSplit_2fca346db6561.nwd | 1 + tests/reference/guiSplit_31489056e0916.nwd | 1 + tests/reference/guiSplit_41cfc0d1f2d12.nwd | 1 + tests/reference/guiSplit_98010bd9270f9.nwd | 1 + tests/test_core/test_core_document.py | 6 ++++++ tests/test_gui/test_gui_doceditor.py | 14 ++++++++----- tests/test_gui/test_gui_mergesplit.py | 20 +++++++++---------- 16 files changed, 38 insertions(+), 15 deletions(-) diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index be611dce..916d98df 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,6 +1,7 @@ %%~name: Making a Scene %%~path: e7ded148d6e4a/636b6aa9b697b %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:30:48 ### Making a Scene @pov: Jane diff --git a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd index acb36501..74cf27b3 100644 --- a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd +++ b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd @@ -1,6 +1,7 @@ %%~name: New File %%~path: 44cb730c42048/031b4af5197ec %%~kind: PLOT/NOTE +%%~time: 2021-01-30 18:49:59 # Main Plot @tag: MainPlot diff --git a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd b/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd index c77c3cd6..ef7ba417 100644 --- a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd +++ b/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd @@ -1,6 +1,7 @@ %%~name: New Scene %%~path: 31489056e0916/0e17daca5f3e1 %%~kind: NOVEL/BOOK +%%~time: 2021-01-30 18:51:35 # Novel ## Chapter diff --git a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd index 9a3ca0a9..7199f069 100644 --- a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd +++ b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd @@ -1,6 +1,7 @@ %%~name: New File %%~path: 71ee45a3c0db9/1a6562590ef19 %%~kind: CHARACTER/NOTE +%%~time: 2021-01-30 18:51:04 # Jane Doe @tag: Jane diff --git a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd index 8e8cb037..d67575a5 100644 --- a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd +++ b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd @@ -1,6 +1,7 @@ %%~name: New File %%~path: 811786ad1ae74/41cfc0d1f2d12 %%~kind: WORLD/NOTE +%%~time: 2021-01-30 18:51:53 # Main Location @tag: Home diff --git a/tests/reference/guiMerge_73475cb40a568.nwd b/tests/reference/guiMerge_73475cb40a568.nwd index 5a903143..38a094d6 100644 --- a/tests/reference/guiMerge_73475cb40a568.nwd +++ b/tests/reference/guiMerge_73475cb40a568.nwd @@ -1,6 +1,7 @@ %%~name: Chapter One %%~path: b3643d0f92e32/73475cb40a568 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:39:07 ## Chapter One @pov: Bod diff --git a/tests/reference/guiSplit_031b4af5197ec.nwd b/tests/reference/guiSplit_031b4af5197ec.nwd index cbaf3205..bd819978 100644 --- a/tests/reference/guiSplit_031b4af5197ec.nwd +++ b/tests/reference/guiSplit_031b4af5197ec.nwd @@ -1,6 +1,7 @@ %%~name: Scene One %%~path: 0e17daca5f3e1/031b4af5197ec %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:42:30 ### Scene One @pov: Bod diff --git a/tests/reference/guiSplit_25fc0e7096fc6.nwd b/tests/reference/guiSplit_25fc0e7096fc6.nwd index 3247412b..e6d495ea 100644 --- a/tests/reference/guiSplit_25fc0e7096fc6.nwd +++ b/tests/reference/guiSplit_25fc0e7096fc6.nwd @@ -1,6 +1,7 @@ %%~name: Chapter One %%~path: 811786ad1ae74/25fc0e7096fc6 %%~kind: NOVEL/CHAPTER +%%~time: 2021-01-30 18:40:32 ## Chapter One @pov: Bod diff --git a/tests/reference/guiSplit_2858dcd1057d3.nwd b/tests/reference/guiSplit_2858dcd1057d3.nwd index c1e79bdf..64292037 100644 --- a/tests/reference/guiSplit_2858dcd1057d3.nwd +++ b/tests/reference/guiSplit_2858dcd1057d3.nwd @@ -1,6 +1,7 @@ %%~name: Scene Two %%~path: 0e17daca5f3e1/2858dcd1057d3 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:43:17 ### Scene Two @pov: Bod diff --git a/tests/reference/guiSplit_2fca346db6561.nwd b/tests/reference/guiSplit_2fca346db6561.nwd index c33e7901..f818dc02 100644 --- a/tests/reference/guiSplit_2fca346db6561.nwd +++ b/tests/reference/guiSplit_2fca346db6561.nwd @@ -1,6 +1,7 @@ %%~name: Scene Two, Section Two %%~path: 0e17daca5f3e1/2fca346db6561 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:43:30 #### Scene Two, Section Two Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. diff --git a/tests/reference/guiSplit_31489056e0916.nwd b/tests/reference/guiSplit_31489056e0916.nwd index e494f6b9..9d770c6c 100644 --- a/tests/reference/guiSplit_31489056e0916.nwd +++ b/tests/reference/guiSplit_31489056e0916.nwd @@ -1,6 +1,7 @@ %%~name: Scene One %%~path: 811786ad1ae74/31489056e0916 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:41:22 ### Scene One @pov: Bod diff --git a/tests/reference/guiSplit_41cfc0d1f2d12.nwd b/tests/reference/guiSplit_41cfc0d1f2d12.nwd index 6f887f44..1c91f150 100644 --- a/tests/reference/guiSplit_41cfc0d1f2d12.nwd +++ b/tests/reference/guiSplit_41cfc0d1f2d12.nwd @@ -1,6 +1,7 @@ %%~name: Scene One, Section Two %%~path: 0e17daca5f3e1/41cfc0d1f2d12 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:43:04 #### Scene One, Section Two Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. diff --git a/tests/reference/guiSplit_98010bd9270f9.nwd b/tests/reference/guiSplit_98010bd9270f9.nwd index 0725f6a5..451a4bb5 100644 --- a/tests/reference/guiSplit_98010bd9270f9.nwd +++ b/tests/reference/guiSplit_98010bd9270f9.nwd @@ -1,6 +1,7 @@ %%~name: Scene Two %%~path: 811786ad1ae74/98010bd9270f9 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:41:48 ### Scene Two @pov: Bod diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index bbfe658b..a84e6972 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -28,6 +28,7 @@ from dummy import causeOSError from nw.core import NWProject, NWDoc from nw.core.item import NWItem from nw.constants import nwItemClass, nwItemLayout +from nw.common import formatTimeStamp @pytest.mark.core def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): @@ -74,6 +75,7 @@ def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): assert theDoc.saveDocument(theText) # Save again to ensure temp file and previous file is handled + monkeypatch.setattr("nw.core.document.time", lambda: 123.4) assert theDoc.saveDocument(theText) # Check file content @@ -83,9 +85,11 @@ def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): "%%~name: New File\n" f"%%~path: a508bb932959c/{xHandle}\n" "%%~kind: NOVEL/SCENE\n" + f"%%~time: {formatTimeStamp(123.4)}\n" "### Test File\n\n" "Text ...\n\n" ) + monkeypatch.undo() # Force no meta data theDoc._theItem = None @@ -147,12 +151,14 @@ def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal): assert theLayout == nwItemLayout.SCENE # Add meta data garbage + monkeypatch.setattr("nw.core.document.time", lambda: 123.4) assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n") with open(docPath, mode="r", encoding="utf8") as inFile: assert inFile.read() == ( "%%~name: New Scene\n" f"%%~path: a6d311a93600a/{sHandle}\n" "%%~kind: NOVEL/SCENE\n" + f"%%~time: {formatTimeStamp(123.4)}\n" "%%~ stuff\n" "### Test File\n\n" "Text ...\n\n" diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 6d0034c7..94931ce5 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -28,8 +28,9 @@ from tools import cmpFiles from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from nw.gui.itemeditor import GuiItemEditor from nw.constants import nwItemType, nwDocAction keyDelay = 2 @@ -42,6 +43,9 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None) + monkeypatch.setattr(GuiItemEditor, "result", lambda *args: QDialog.Accepted) # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) @@ -321,25 +325,25 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_031b4af5197ec.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(fncProj, "content", "1a6562590ef19.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_1a6562590ef19.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_1a6562590ef19.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(fncProj, "content", "0e17daca5f3e1.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(fncProj, "content", "41cfc0d1f2d12.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) # qtbot.stopForInteraction() diff --git a/tests/test_gui/test_gui_mergesplit.py b/tests/test_gui/test_gui_mergesplit.py index b2d0135e..d10c2394 100644 --- a/tests/test_gui/test_gui_mergesplit.py +++ b/tests/test_gui/test_gui_mergesplit.py @@ -66,7 +66,7 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiMerge_73475cb40a568.nwd") compFile = os.path.join(refDir, "guiMerge_73475cb40a568.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) # Split By Chapter assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -92,7 +92,7 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiMerge_71ee45a3c0db9.nwd") compFile = os.path.join(refDir, "guiMerge_73475cb40a568.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [1, 2, 3]) + assert cmpFiles(testFile, compFile, [1, 2, 3, 4]) # Split By Scene assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -116,19 +116,19 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiSplit_25fc0e7096fc6.nwd") compFile = os.path.join(refDir, "guiSplit_25fc0e7096fc6.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd") testFile = os.path.join(outDir, "guiSplit_31489056e0916.nwd") compFile = os.path.join(refDir, "guiSplit_31489056e0916.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd") testFile = os.path.join(outDir, "guiSplit_98010bd9270f9.nwd") compFile = os.path.join(refDir, "guiSplit_98010bd9270f9.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) # Split By Section assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -154,31 +154,31 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiSplit_1a6562590ef19.nwd") compFile = os.path.join(refDir, "guiSplit_25fc0e7096fc6.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [1, 2, 3]) + assert cmpFiles(testFile, compFile, [1, 2, 3, 4]) projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd") testFile = os.path.join(outDir, "guiSplit_031b4af5197ec.nwd") compFile = os.path.join(refDir, "guiSplit_031b4af5197ec.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd") testFile = os.path.join(outDir, "guiSplit_41cfc0d1f2d12.nwd") compFile = os.path.join(refDir, "guiSplit_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd") testFile = os.path.join(outDir, "guiSplit_2858dcd1057d3.nwd") compFile = os.path.join(refDir, "guiSplit_2858dcd1057d3.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd") testFile = os.path.join(outDir, "guiSplit_2fca346db6561.nwd") compFile = os.path.join(refDir, "guiSplit_2fca346db6561.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, [4]) # qtbot.stopForInteraction() From 4727866b1503fcd1ea08b0e846bec73cb1def9bb Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 19:00:12 +0100 Subject: [PATCH 036/104] Update test projects --- tests/lipsum/ToC.txt | 39 ++++++++++++------------- tests/lipsum/content/04468803b92e1.nwd | 1 + tests/lipsum/content/2426c6f0ca922.nwd | 1 + tests/lipsum/content/441420a886d82.nwd | 1 + tests/lipsum/content/47666c91c7ccf.nwd | 1 + tests/lipsum/content/4c4f28287af27.nwd | 1 + tests/lipsum/content/7a992350f3eb6.nwd | 1 + tests/lipsum/content/846352075de7d.nwd | 1 + tests/lipsum/content/88243afbe5ed8.nwd | 1 + tests/lipsum/content/88d59a277361b.nwd | 1 + tests/lipsum/content/8c58a65414c23.nwd | 1 + tests/lipsum/content/db7e733775d4d.nwd | 1 + tests/lipsum/content/eb103bc70c90c.nwd | 1 + tests/lipsum/content/f8c0562e50f1b.nwd | 1 + tests/lipsum/content/f96ec11c6a3da.nwd | 1 + tests/lipsum/content/fb609cd8319dc.nwd | 1 + tests/lipsum/nwProject.nwx | 8 ++--- tests/minimal/ToC.txt | 15 +++++----- tests/minimal/content/8c659a11cd429.nwd | 1 + tests/minimal/content/a35baf2e93843.nwd | 1 + tests/minimal/content/f5ab3e30151e1.nwd | 1 + tests/minimal/nwProject.nwx | 10 +++---- 22 files changed, 53 insertions(+), 37 deletions(-) diff --git a/tests/lipsum/ToC.txt b/tests/lipsum/ToC.txt index a499c6f8..2d477f30 100644 --- a/tests/lipsum/ToC.txt +++ b/tests/lipsum/ToC.txt @@ -1,22 +1,21 @@ - Table of Contents -=================== - - File Name Class Document Label --------------------------------------------------------------------------------- - content/04468803b92e1.nwd WORLD Ancient Europe - content/2426c6f0ca922.nwd PLOT Main - content/441420a886d82.nwd NOVEL Chapter Two - content/47666c91c7ccf.nwd NOVEL Scene Five - content/4c4f28287af27.nwd CHARACTER Mr. Nobody - content/7a992350f3eb6.nwd NOVEL Lorem Ipsum - content/846352075de7d.nwd NOVEL Interlude - content/88243afbe5ed8.nwd NOVEL Scene One - content/88d59a277361b.nwd NOVEL Prologue - content/8c58a65414c23.nwd NOVEL Front Matter - content/db7e733775d4d.nwd NOVEL Act One - content/eb103bc70c90c.nwd NOVEL Scene Three - content/f8c0562e50f1b.nwd NOVEL Scene Four - content/f96ec11c6a3da.nwd NOVEL Scene Two - content/fb609cd8319dc.nwd NOVEL Chapter One +Table of Contents +================= +File Name Class Layout Document Label +---------------------------------------------------------------- +content/7a992350f3eb6.nwd NOVEL TITLE Lorem Ipsum +content/8c58a65414c23.nwd NOVEL PAGE Front Matter +content/88d59a277361b.nwd NOVEL UNNUMBERED Prologue +content/db7e733775d4d.nwd NOVEL PARTITION Act One +content/fb609cd8319dc.nwd NOVEL CHAPTER Chapter One +content/88243afbe5ed8.nwd NOVEL SCENE Scene One +content/f96ec11c6a3da.nwd NOVEL SCENE Scene Two +content/846352075de7d.nwd NOVEL BOOK Interlude +content/441420a886d82.nwd NOVEL CHAPTER Chapter Two +content/eb103bc70c90c.nwd NOVEL SCENE Scene Three +content/f8c0562e50f1b.nwd NOVEL SCENE Scene Four +content/47666c91c7ccf.nwd NOVEL SCENE Scene Five +content/4c4f28287af27.nwd CHARACTER NOTE Mr. Nobody +content/2426c6f0ca922.nwd PLOT NOTE Main +content/04468803b92e1.nwd WORLD NOTE Ancient Europe diff --git a/tests/lipsum/content/04468803b92e1.nwd b/tests/lipsum/content/04468803b92e1.nwd index 6d706890..5534a8cd 100644 --- a/tests/lipsum/content/04468803b92e1.nwd +++ b/tests/lipsum/content/04468803b92e1.nwd @@ -1,6 +1,7 @@ %%~name: Ancient Europe %%~path: 60bdf227455cc/04468803b92e1 %%~kind: WORLD/NOTE +%%~time: 2021-01-30 18:55:56 # Ancient Europe @tag: Europe diff --git a/tests/lipsum/content/2426c6f0ca922.nwd b/tests/lipsum/content/2426c6f0ca922.nwd index ad926141..cc4f8a87 100644 --- a/tests/lipsum/content/2426c6f0ca922.nwd +++ b/tests/lipsum/content/2426c6f0ca922.nwd @@ -1,6 +1,7 @@ %%~name: Main %%~path: 6c6afb1247750/2426c6f0ca922 %%~kind: PLOT/NOTE +%%~time: 2021-01-30 18:55:55 # Main Plot @tag: Main diff --git a/tests/lipsum/content/441420a886d82.nwd b/tests/lipsum/content/441420a886d82.nwd index 26237180..904a77f4 100644 --- a/tests/lipsum/content/441420a886d82.nwd +++ b/tests/lipsum/content/441420a886d82.nwd @@ -1,6 +1,7 @@ %%~name: Chapter Two %%~path: 6bd935d2490cd/441420a886d82 %%~kind: NOVEL/CHAPTER +%%~time: 2021-01-30 18:55:50 ## Chapter Two @pov: Bod diff --git a/tests/lipsum/content/47666c91c7ccf.nwd b/tests/lipsum/content/47666c91c7ccf.nwd index 7ea17223..76204acd 100644 --- a/tests/lipsum/content/47666c91c7ccf.nwd +++ b/tests/lipsum/content/47666c91c7ccf.nwd @@ -1,6 +1,7 @@ %%~name: Scene Five %%~path: 6bd935d2490cd/47666c91c7ccf %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:55:53 ### Scene Five @pov: Bod diff --git a/tests/lipsum/content/4c4f28287af27.nwd b/tests/lipsum/content/4c4f28287af27.nwd index d845442f..146e66f0 100644 --- a/tests/lipsum/content/4c4f28287af27.nwd +++ b/tests/lipsum/content/4c4f28287af27.nwd @@ -1,6 +1,7 @@ %%~name: Mr. Nobody %%~path: 67a8707f2f249/4c4f28287af27 %%~kind: CHARACTER/NOTE +%%~time: 2021-01-30 18:55:54 # Nobody Owens @tag: Bod diff --git a/tests/lipsum/content/7a992350f3eb6.nwd b/tests/lipsum/content/7a992350f3eb6.nwd index 6982e548..30db992a 100644 --- a/tests/lipsum/content/7a992350f3eb6.nwd +++ b/tests/lipsum/content/7a992350f3eb6.nwd @@ -1,6 +1,7 @@ %%~name: Lorem Ipsum %%~path: b3643d0f92e32/7a992350f3eb6 %%~kind: NOVEL/TITLE +%%~time: 2021-01-30 18:56:00 # Lorem Ipsum **By lipsum.com** diff --git a/tests/lipsum/content/846352075de7d.nwd b/tests/lipsum/content/846352075de7d.nwd index d362ccc6..b7f9c728 100644 --- a/tests/lipsum/content/846352075de7d.nwd +++ b/tests/lipsum/content/846352075de7d.nwd @@ -1,6 +1,7 @@ %%~name: Interlude %%~path: b3643d0f92e32/846352075de7d %%~kind: NOVEL/BOOK +%%~time: 2021-01-30 18:55:49 ## Why do we use it? % Exctracted from the lipsum.com website. diff --git a/tests/lipsum/content/88243afbe5ed8.nwd b/tests/lipsum/content/88243afbe5ed8.nwd index 426ffeba..966d97a4 100644 --- a/tests/lipsum/content/88243afbe5ed8.nwd +++ b/tests/lipsum/content/88243afbe5ed8.nwd @@ -1,6 +1,7 @@ %%~name: Scene One %%~path: 45e6b01ca35c1/88243afbe5ed8 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:55:47 ### Scene One @pov: Bod diff --git a/tests/lipsum/content/88d59a277361b.nwd b/tests/lipsum/content/88d59a277361b.nwd index 4d55bfda..458cf36b 100644 --- a/tests/lipsum/content/88d59a277361b.nwd +++ b/tests/lipsum/content/88d59a277361b.nwd @@ -1,6 +1,7 @@ %%~name: Prologue %%~path: b3643d0f92e32/88d59a277361b %%~kind: NOVEL/UNNUMBERED +%%~time: 2021-01-30 18:55:42 ## Prologue % Synopsis:Explanation from the lipsum.com website. diff --git a/tests/lipsum/content/8c58a65414c23.nwd b/tests/lipsum/content/8c58a65414c23.nwd index 28e54bef..e703e53c 100644 --- a/tests/lipsum/content/8c58a65414c23.nwd +++ b/tests/lipsum/content/8c58a65414c23.nwd @@ -1,6 +1,7 @@ %%~name: Front Matter %%~path: b3643d0f92e32/8c58a65414c23 %%~kind: NOVEL/PAGE +%%~time: 2021-01-30 18:55:40 % Exctracted from the lipsum.com website. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. diff --git a/tests/lipsum/content/db7e733775d4d.nwd b/tests/lipsum/content/db7e733775d4d.nwd index d677152f..a1d60c9d 100644 --- a/tests/lipsum/content/db7e733775d4d.nwd +++ b/tests/lipsum/content/db7e733775d4d.nwd @@ -1,6 +1,7 @@ %%~name: Act One %%~path: b3643d0f92e32/db7e733775d4d %%~kind: NOVEL/PARTITION +%%~time: 2021-01-30 18:55:45 # Act One “Fusce maximus felis libero” \ No newline at end of file diff --git a/tests/lipsum/content/eb103bc70c90c.nwd b/tests/lipsum/content/eb103bc70c90c.nwd index 65ce7e49..2db52f1f 100644 --- a/tests/lipsum/content/eb103bc70c90c.nwd +++ b/tests/lipsum/content/eb103bc70c90c.nwd @@ -1,6 +1,7 @@ %%~name: Scene Three %%~path: 6bd935d2490cd/eb103bc70c90c %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:55:51 ### Scene Three @pov: Bod diff --git a/tests/lipsum/content/f8c0562e50f1b.nwd b/tests/lipsum/content/f8c0562e50f1b.nwd index f8218e1f..3e6ffafd 100644 --- a/tests/lipsum/content/f8c0562e50f1b.nwd +++ b/tests/lipsum/content/f8c0562e50f1b.nwd @@ -1,6 +1,7 @@ %%~name: Scene Four %%~path: 6bd935d2490cd/f8c0562e50f1b %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:55:52 ### Scene Four @pov: Bod diff --git a/tests/lipsum/content/f96ec11c6a3da.nwd b/tests/lipsum/content/f96ec11c6a3da.nwd index 60853dc2..6f48cd41 100644 --- a/tests/lipsum/content/f96ec11c6a3da.nwd +++ b/tests/lipsum/content/f96ec11c6a3da.nwd @@ -1,6 +1,7 @@ %%~name: Scene Two %%~path: 45e6b01ca35c1/f96ec11c6a3da %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:55:48 ### Scene Two @pov: Bod diff --git a/tests/lipsum/content/fb609cd8319dc.nwd b/tests/lipsum/content/fb609cd8319dc.nwd index ff48ad12..a52f2e4b 100644 --- a/tests/lipsum/content/fb609cd8319dc.nwd +++ b/tests/lipsum/content/fb609cd8319dc.nwd @@ -1,6 +1,7 @@ %%~name: Chapter One %%~path: 45e6b01ca35c1/fb609cd8319dc %%~kind: NOVEL/CHAPTER +%%~time: 2021-01-30 18:55:46 ## Chapter One @pov: Bod diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 5dec55bb..a4e5bcc2 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 10 - 22 - 1571 + 12 + 23 + 1650 False diff --git a/tests/minimal/ToC.txt b/tests/minimal/ToC.txt index f5b94b72..a41be0bf 100644 --- a/tests/minimal/ToC.txt +++ b/tests/minimal/ToC.txt @@ -1,10 +1,9 @@ - Table of Contents -=================== - - File Name Class Document Label --------------------------------------------------------------------------------- - content/8c659a11cd429.nwd NOVEL New Scene - content/a35baf2e93843.nwd NOVEL Title Page - content/f5ab3e30151e1.nwd NOVEL New Chapter +Table of Contents +================= +File Name Class Layout Document Label +------------------------------------------------------------- +content/a35baf2e93843.nwd NOVEL TITLE Title Page +content/f5ab3e30151e1.nwd NOVEL CHAPTER New Chapter +content/8c659a11cd429.nwd NOVEL SCENE New Scene diff --git a/tests/minimal/content/8c659a11cd429.nwd b/tests/minimal/content/8c659a11cd429.nwd index bdb8079f..1607c667 100644 --- a/tests/minimal/content/8c659a11cd429.nwd +++ b/tests/minimal/content/8c659a11cd429.nwd @@ -1,5 +1,6 @@ %%~name: New Scene %%~path: a6d311a93600a/8c659a11cd429 %%~kind: NOVEL/SCENE +%%~time: 2021-01-30 18:56:20 ### New Scene diff --git a/tests/minimal/content/a35baf2e93843.nwd b/tests/minimal/content/a35baf2e93843.nwd index a1120152..8b9b738b 100644 --- a/tests/minimal/content/a35baf2e93843.nwd +++ b/tests/minimal/content/a35baf2e93843.nwd @@ -1,6 +1,7 @@ %%~name: Title Page %%~path: a508bb932959c/a35baf2e93843 %%~kind: NOVEL/TITLE +%%~time: 2021-01-30 18:56:17 # Minimal By Jane Doe, John Doh diff --git a/tests/minimal/content/f5ab3e30151e1.nwd b/tests/minimal/content/f5ab3e30151e1.nwd index f08335a0..5a8659be 100644 --- a/tests/minimal/content/f5ab3e30151e1.nwd +++ b/tests/minimal/content/f5ab3e30151e1.nwd @@ -1,5 +1,6 @@ %%~name: New Chapter %%~path: a6d311a93600a/f5ab3e30151e1 %%~kind: NOVEL/CHAPTER +%%~time: 2021-01-30 18:56:19 ## New Chapter diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index d45f9bfa..c7bc1f3f 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 3 - 1 - 33 + 4 + 2 + 83 True @@ -65,7 +65,7 @@ FOLDER NOVEL New - False + True New Chapter From 96d6accbde799352fa284e7dd3a2ac24fff38e9d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 22:55:38 +0100 Subject: [PATCH 037/104] Add layout and class info to editor footer bar --- nw/gui/doceditor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 31e77f0d..bc83719b 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -54,7 +54,7 @@ from nw.gui.dochighlight import GuiDocHighlighter from nw.common import transferCase from nw.constants import ( nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass, - nwKeyWords + nwKeyWords, nwLabels ) logger = logging.getLogger(__name__) @@ -417,6 +417,7 @@ class GuiDocEditor(QTextEdit): if self.theProject.projTree.updateItemLayout(tHandle, hLevel): self.theParent.treeView.setTreeItemValues(tHandle) self.nwDocument.saveDocument(docText) + self.docFooter.updateInfo() return True @@ -2484,8 +2485,11 @@ class GuiDocEditFooter(QWidget): else: iStatus = self.theProject.importItems.checkEntry(iStatus) theIcon = self.theParent.importIcons[iStatus] + sIcon = theIcon.pixmap(self.sPx, self.sPx) - sText = self.theItem.itemStatus + sClass = nwLabels.CLASS_NAME[self.theItem.itemClass] + sLayout = nwLabels.LAYOUT_NAME[self.theItem.itemLayout] + sText = f"{self.theItem.itemStatus} / {sClass} / {sLayout}" self.statusIcon.setPixmap(sIcon) self.statusText.setText(sText) From 9e294529cf6db8f968dbe33836e4dea53707aa25 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 Jan 2021 23:31:19 +0100 Subject: [PATCH 038/104] When a new file is added, automatically add the title line --- nw/gui/projtree.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index ca4c11de..f2703445 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -269,10 +269,43 @@ class GuiProjectTree(QTreeWidget): logger.error("Failed to add new item") return False + # If there is no handle set, return here + if tHandle is None: + return True + # Add the new item to the tree - if tHandle is not None: - self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) + self.revealNewTreeItem(tHandle, nHandle) + self.theParent.editItem(tHandle) + nwItem = self.theProject.projTree[tHandle] + + # If this is a folder, return here + if nwItem.itemType != nwItemType.FILE: + return True + + # This is a new files, so let's add some content + newDoc = NWDoc(self.theProject, self.theParent) + curTxt = newDoc.openDocument(tHandle, showStatus=False) + if curTxt == "": + if nwItem.itemLayout == nwItemLayout.CHAPTER: + newText = f"## {nwItem.itemName}\n\n" + elif nwItem.itemLayout == nwItemLayout.UNNUMBERED: + newText = f"## {nwItem.itemName}\n\n" + elif nwItem.itemLayout == nwItemLayout.SCENE: + newText = f"### {nwItem.itemName}\n\n" + else: + newText = f"# {nwItem.itemName}\n\n" + + # Save the text and index it + newDoc.saveDocument(newText) + self.theParent.theIndex.scanText(tHandle, newText) + + # Get Word Counts + cC, wC, pC = self.theParent.theIndex.getCounts(tHandle) + nwItem.setCharCount(cC) + nwItem.setWordCount(wC) + nwItem.setParaCount(pC) + self.propagateCount(tHandle, wC) + self.projectWordCount() return True From 2df0d81d29a79b5a3a2416e98beada61e03277c0 Mon Sep 17 00:00:00 2001 From: "Veronica K. Berglyd Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 11:59:24 +0100 Subject: [PATCH 039/104] Revert "Add timestamp to document meta" --- nw/core/document.py | 5 +-- sample/content/636b6aa9b697b.nwd | 1 - tests/lipsum/ToC.txt | 39 ++++++++++--------- tests/lipsum/content/04468803b92e1.nwd | 1 - tests/lipsum/content/2426c6f0ca922.nwd | 1 - tests/lipsum/content/441420a886d82.nwd | 1 - tests/lipsum/content/47666c91c7ccf.nwd | 1 - tests/lipsum/content/4c4f28287af27.nwd | 1 - tests/lipsum/content/7a992350f3eb6.nwd | 1 - tests/lipsum/content/846352075de7d.nwd | 1 - tests/lipsum/content/88243afbe5ed8.nwd | 1 - tests/lipsum/content/88d59a277361b.nwd | 1 - tests/lipsum/content/8c58a65414c23.nwd | 1 - tests/lipsum/content/db7e733775d4d.nwd | 1 - tests/lipsum/content/eb103bc70c90c.nwd | 1 - tests/lipsum/content/f8c0562e50f1b.nwd | 1 - tests/lipsum/content/f96ec11c6a3da.nwd | 1 - tests/lipsum/content/fb609cd8319dc.nwd | 1 - tests/lipsum/nwProject.nwx | 8 ++-- tests/minimal/ToC.txt | 15 +++---- tests/minimal/content/8c659a11cd429.nwd | 1 - tests/minimal/content/a35baf2e93843.nwd | 1 - tests/minimal/content/f5ab3e30151e1.nwd | 1 - tests/minimal/nwProject.nwx | 10 ++--- .../guiEditor_Main_Final_031b4af5197ec.nwd | 1 - .../guiEditor_Main_Final_0e17daca5f3e1.nwd | 1 - .../guiEditor_Main_Final_1a6562590ef19.nwd | 1 - .../guiEditor_Main_Final_41cfc0d1f2d12.nwd | 1 - tests/reference/guiMerge_73475cb40a568.nwd | 1 - tests/reference/guiSplit_031b4af5197ec.nwd | 1 - tests/reference/guiSplit_25fc0e7096fc6.nwd | 1 - tests/reference/guiSplit_2858dcd1057d3.nwd | 1 - tests/reference/guiSplit_2fca346db6561.nwd | 1 - tests/reference/guiSplit_31489056e0916.nwd | 1 - tests/reference/guiSplit_41cfc0d1f2d12.nwd | 1 - tests/reference/guiSplit_98010bd9270f9.nwd | 1 - tests/test_core/test_core_document.py | 6 --- tests/test_gui/test_gui_doceditor.py | 14 +++---- tests/test_gui/test_gui_mergesplit.py | 20 +++++----- 39 files changed, 53 insertions(+), 95 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 42db1603..7e833854 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -27,10 +27,8 @@ along with this program. If not, see . import logging import os -from time import time - from nw.constants import nwAlert -from nw.common import isHandle, formatTimeStamp +from nw.common import isHandle from nw.constants import nwItemLayout, nwItemClass logger = logging.getLogger(__name__) @@ -153,7 +151,6 @@ class NWDoc(): f"%%~name: {self._theItem.itemName}\n" f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n" f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n" - f"%%~time: {formatTimeStamp(time())}\n" ) try: diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 916d98df..be611dce 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,7 +1,6 @@ %%~name: Making a Scene %%~path: e7ded148d6e4a/636b6aa9b697b %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:30:48 ### Making a Scene @pov: Jane diff --git a/tests/lipsum/ToC.txt b/tests/lipsum/ToC.txt index 2d477f30..a499c6f8 100644 --- a/tests/lipsum/ToC.txt +++ b/tests/lipsum/ToC.txt @@ -1,21 +1,22 @@ -Table of Contents -================= + Table of Contents +=================== + + File Name Class Document Label +-------------------------------------------------------------------------------- + content/04468803b92e1.nwd WORLD Ancient Europe + content/2426c6f0ca922.nwd PLOT Main + content/441420a886d82.nwd NOVEL Chapter Two + content/47666c91c7ccf.nwd NOVEL Scene Five + content/4c4f28287af27.nwd CHARACTER Mr. Nobody + content/7a992350f3eb6.nwd NOVEL Lorem Ipsum + content/846352075de7d.nwd NOVEL Interlude + content/88243afbe5ed8.nwd NOVEL Scene One + content/88d59a277361b.nwd NOVEL Prologue + content/8c58a65414c23.nwd NOVEL Front Matter + content/db7e733775d4d.nwd NOVEL Act One + content/eb103bc70c90c.nwd NOVEL Scene Three + content/f8c0562e50f1b.nwd NOVEL Scene Four + content/f96ec11c6a3da.nwd NOVEL Scene Two + content/fb609cd8319dc.nwd NOVEL Chapter One -File Name Class Layout Document Label ----------------------------------------------------------------- -content/7a992350f3eb6.nwd NOVEL TITLE Lorem Ipsum -content/8c58a65414c23.nwd NOVEL PAGE Front Matter -content/88d59a277361b.nwd NOVEL UNNUMBERED Prologue -content/db7e733775d4d.nwd NOVEL PARTITION Act One -content/fb609cd8319dc.nwd NOVEL CHAPTER Chapter One -content/88243afbe5ed8.nwd NOVEL SCENE Scene One -content/f96ec11c6a3da.nwd NOVEL SCENE Scene Two -content/846352075de7d.nwd NOVEL BOOK Interlude -content/441420a886d82.nwd NOVEL CHAPTER Chapter Two -content/eb103bc70c90c.nwd NOVEL SCENE Scene Three -content/f8c0562e50f1b.nwd NOVEL SCENE Scene Four -content/47666c91c7ccf.nwd NOVEL SCENE Scene Five -content/4c4f28287af27.nwd CHARACTER NOTE Mr. Nobody -content/2426c6f0ca922.nwd PLOT NOTE Main -content/04468803b92e1.nwd WORLD NOTE Ancient Europe diff --git a/tests/lipsum/content/04468803b92e1.nwd b/tests/lipsum/content/04468803b92e1.nwd index 5534a8cd..6d706890 100644 --- a/tests/lipsum/content/04468803b92e1.nwd +++ b/tests/lipsum/content/04468803b92e1.nwd @@ -1,7 +1,6 @@ %%~name: Ancient Europe %%~path: 60bdf227455cc/04468803b92e1 %%~kind: WORLD/NOTE -%%~time: 2021-01-30 18:55:56 # Ancient Europe @tag: Europe diff --git a/tests/lipsum/content/2426c6f0ca922.nwd b/tests/lipsum/content/2426c6f0ca922.nwd index cc4f8a87..ad926141 100644 --- a/tests/lipsum/content/2426c6f0ca922.nwd +++ b/tests/lipsum/content/2426c6f0ca922.nwd @@ -1,7 +1,6 @@ %%~name: Main %%~path: 6c6afb1247750/2426c6f0ca922 %%~kind: PLOT/NOTE -%%~time: 2021-01-30 18:55:55 # Main Plot @tag: Main diff --git a/tests/lipsum/content/441420a886d82.nwd b/tests/lipsum/content/441420a886d82.nwd index 904a77f4..26237180 100644 --- a/tests/lipsum/content/441420a886d82.nwd +++ b/tests/lipsum/content/441420a886d82.nwd @@ -1,7 +1,6 @@ %%~name: Chapter Two %%~path: 6bd935d2490cd/441420a886d82 %%~kind: NOVEL/CHAPTER -%%~time: 2021-01-30 18:55:50 ## Chapter Two @pov: Bod diff --git a/tests/lipsum/content/47666c91c7ccf.nwd b/tests/lipsum/content/47666c91c7ccf.nwd index 76204acd..7ea17223 100644 --- a/tests/lipsum/content/47666c91c7ccf.nwd +++ b/tests/lipsum/content/47666c91c7ccf.nwd @@ -1,7 +1,6 @@ %%~name: Scene Five %%~path: 6bd935d2490cd/47666c91c7ccf %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:55:53 ### Scene Five @pov: Bod diff --git a/tests/lipsum/content/4c4f28287af27.nwd b/tests/lipsum/content/4c4f28287af27.nwd index 146e66f0..d845442f 100644 --- a/tests/lipsum/content/4c4f28287af27.nwd +++ b/tests/lipsum/content/4c4f28287af27.nwd @@ -1,7 +1,6 @@ %%~name: Mr. Nobody %%~path: 67a8707f2f249/4c4f28287af27 %%~kind: CHARACTER/NOTE -%%~time: 2021-01-30 18:55:54 # Nobody Owens @tag: Bod diff --git a/tests/lipsum/content/7a992350f3eb6.nwd b/tests/lipsum/content/7a992350f3eb6.nwd index 30db992a..6982e548 100644 --- a/tests/lipsum/content/7a992350f3eb6.nwd +++ b/tests/lipsum/content/7a992350f3eb6.nwd @@ -1,7 +1,6 @@ %%~name: Lorem Ipsum %%~path: b3643d0f92e32/7a992350f3eb6 %%~kind: NOVEL/TITLE -%%~time: 2021-01-30 18:56:00 # Lorem Ipsum **By lipsum.com** diff --git a/tests/lipsum/content/846352075de7d.nwd b/tests/lipsum/content/846352075de7d.nwd index b7f9c728..d362ccc6 100644 --- a/tests/lipsum/content/846352075de7d.nwd +++ b/tests/lipsum/content/846352075de7d.nwd @@ -1,7 +1,6 @@ %%~name: Interlude %%~path: b3643d0f92e32/846352075de7d %%~kind: NOVEL/BOOK -%%~time: 2021-01-30 18:55:49 ## Why do we use it? % Exctracted from the lipsum.com website. diff --git a/tests/lipsum/content/88243afbe5ed8.nwd b/tests/lipsum/content/88243afbe5ed8.nwd index 966d97a4..426ffeba 100644 --- a/tests/lipsum/content/88243afbe5ed8.nwd +++ b/tests/lipsum/content/88243afbe5ed8.nwd @@ -1,7 +1,6 @@ %%~name: Scene One %%~path: 45e6b01ca35c1/88243afbe5ed8 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:55:47 ### Scene One @pov: Bod diff --git a/tests/lipsum/content/88d59a277361b.nwd b/tests/lipsum/content/88d59a277361b.nwd index 458cf36b..4d55bfda 100644 --- a/tests/lipsum/content/88d59a277361b.nwd +++ b/tests/lipsum/content/88d59a277361b.nwd @@ -1,7 +1,6 @@ %%~name: Prologue %%~path: b3643d0f92e32/88d59a277361b %%~kind: NOVEL/UNNUMBERED -%%~time: 2021-01-30 18:55:42 ## Prologue % Synopsis:Explanation from the lipsum.com website. diff --git a/tests/lipsum/content/8c58a65414c23.nwd b/tests/lipsum/content/8c58a65414c23.nwd index e703e53c..28e54bef 100644 --- a/tests/lipsum/content/8c58a65414c23.nwd +++ b/tests/lipsum/content/8c58a65414c23.nwd @@ -1,7 +1,6 @@ %%~name: Front Matter %%~path: b3643d0f92e32/8c58a65414c23 %%~kind: NOVEL/PAGE -%%~time: 2021-01-30 18:55:40 % Exctracted from the lipsum.com website. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. diff --git a/tests/lipsum/content/db7e733775d4d.nwd b/tests/lipsum/content/db7e733775d4d.nwd index a1d60c9d..d677152f 100644 --- a/tests/lipsum/content/db7e733775d4d.nwd +++ b/tests/lipsum/content/db7e733775d4d.nwd @@ -1,7 +1,6 @@ %%~name: Act One %%~path: b3643d0f92e32/db7e733775d4d %%~kind: NOVEL/PARTITION -%%~time: 2021-01-30 18:55:45 # Act One “Fusce maximus felis libero” \ No newline at end of file diff --git a/tests/lipsum/content/eb103bc70c90c.nwd b/tests/lipsum/content/eb103bc70c90c.nwd index 2db52f1f..65ce7e49 100644 --- a/tests/lipsum/content/eb103bc70c90c.nwd +++ b/tests/lipsum/content/eb103bc70c90c.nwd @@ -1,7 +1,6 @@ %%~name: Scene Three %%~path: 6bd935d2490cd/eb103bc70c90c %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:55:51 ### Scene Three @pov: Bod diff --git a/tests/lipsum/content/f8c0562e50f1b.nwd b/tests/lipsum/content/f8c0562e50f1b.nwd index 3e6ffafd..f8218e1f 100644 --- a/tests/lipsum/content/f8c0562e50f1b.nwd +++ b/tests/lipsum/content/f8c0562e50f1b.nwd @@ -1,7 +1,6 @@ %%~name: Scene Four %%~path: 6bd935d2490cd/f8c0562e50f1b %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:55:52 ### Scene Four @pov: Bod diff --git a/tests/lipsum/content/f96ec11c6a3da.nwd b/tests/lipsum/content/f96ec11c6a3da.nwd index 6f48cd41..60853dc2 100644 --- a/tests/lipsum/content/f96ec11c6a3da.nwd +++ b/tests/lipsum/content/f96ec11c6a3da.nwd @@ -1,7 +1,6 @@ %%~name: Scene Two %%~path: 45e6b01ca35c1/f96ec11c6a3da %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:55:48 ### Scene Two @pov: Bod diff --git a/tests/lipsum/content/fb609cd8319dc.nwd b/tests/lipsum/content/fb609cd8319dc.nwd index a52f2e4b..ff48ad12 100644 --- a/tests/lipsum/content/fb609cd8319dc.nwd +++ b/tests/lipsum/content/fb609cd8319dc.nwd @@ -1,7 +1,6 @@ %%~name: Chapter One %%~path: 45e6b01ca35c1/fb609cd8319dc %%~kind: NOVEL/CHAPTER -%%~time: 2021-01-30 18:55:46 ## Chapter One @pov: Bod diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index a4e5bcc2..5dec55bb 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 12 - 23 - 1650 + 10 + 22 + 1571 False diff --git a/tests/minimal/ToC.txt b/tests/minimal/ToC.txt index a41be0bf..f5b94b72 100644 --- a/tests/minimal/ToC.txt +++ b/tests/minimal/ToC.txt @@ -1,9 +1,10 @@ -Table of Contents -================= + Table of Contents +=================== + + File Name Class Document Label +-------------------------------------------------------------------------------- + content/8c659a11cd429.nwd NOVEL New Scene + content/a35baf2e93843.nwd NOVEL Title Page + content/f5ab3e30151e1.nwd NOVEL New Chapter -File Name Class Layout Document Label -------------------------------------------------------------- -content/a35baf2e93843.nwd NOVEL TITLE Title Page -content/f5ab3e30151e1.nwd NOVEL CHAPTER New Chapter -content/8c659a11cd429.nwd NOVEL SCENE New Scene diff --git a/tests/minimal/content/8c659a11cd429.nwd b/tests/minimal/content/8c659a11cd429.nwd index 1607c667..bdb8079f 100644 --- a/tests/minimal/content/8c659a11cd429.nwd +++ b/tests/minimal/content/8c659a11cd429.nwd @@ -1,6 +1,5 @@ %%~name: New Scene %%~path: a6d311a93600a/8c659a11cd429 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:56:20 ### New Scene diff --git a/tests/minimal/content/a35baf2e93843.nwd b/tests/minimal/content/a35baf2e93843.nwd index 8b9b738b..a1120152 100644 --- a/tests/minimal/content/a35baf2e93843.nwd +++ b/tests/minimal/content/a35baf2e93843.nwd @@ -1,7 +1,6 @@ %%~name: Title Page %%~path: a508bb932959c/a35baf2e93843 %%~kind: NOVEL/TITLE -%%~time: 2021-01-30 18:56:17 # Minimal By Jane Doe, John Doh diff --git a/tests/minimal/content/f5ab3e30151e1.nwd b/tests/minimal/content/f5ab3e30151e1.nwd index 5a8659be..f08335a0 100644 --- a/tests/minimal/content/f5ab3e30151e1.nwd +++ b/tests/minimal/content/f5ab3e30151e1.nwd @@ -1,6 +1,5 @@ %%~name: New Chapter %%~path: a6d311a93600a/f5ab3e30151e1 %%~kind: NOVEL/CHAPTER -%%~time: 2021-01-30 18:56:19 ## New Chapter diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index c7bc1f3f..d45f9bfa 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 4 - 2 - 83 + 3 + 1 + 33 True @@ -65,7 +65,7 @@ FOLDER NOVEL New - True + False New Chapter diff --git a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd index 74cf27b3..acb36501 100644 --- a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd +++ b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd @@ -1,7 +1,6 @@ %%~name: New File %%~path: 44cb730c42048/031b4af5197ec %%~kind: PLOT/NOTE -%%~time: 2021-01-30 18:49:59 # Main Plot @tag: MainPlot diff --git a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd b/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd index ef7ba417..c77c3cd6 100644 --- a/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd +++ b/tests/reference/guiEditor_Main_Final_0e17daca5f3e1.nwd @@ -1,7 +1,6 @@ %%~name: New Scene %%~path: 31489056e0916/0e17daca5f3e1 %%~kind: NOVEL/BOOK -%%~time: 2021-01-30 18:51:35 # Novel ## Chapter diff --git a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd index 7199f069..9a3ca0a9 100644 --- a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd +++ b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd @@ -1,7 +1,6 @@ %%~name: New File %%~path: 71ee45a3c0db9/1a6562590ef19 %%~kind: CHARACTER/NOTE -%%~time: 2021-01-30 18:51:04 # Jane Doe @tag: Jane diff --git a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd index d67575a5..8e8cb037 100644 --- a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd +++ b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd @@ -1,7 +1,6 @@ %%~name: New File %%~path: 811786ad1ae74/41cfc0d1f2d12 %%~kind: WORLD/NOTE -%%~time: 2021-01-30 18:51:53 # Main Location @tag: Home diff --git a/tests/reference/guiMerge_73475cb40a568.nwd b/tests/reference/guiMerge_73475cb40a568.nwd index 38a094d6..5a903143 100644 --- a/tests/reference/guiMerge_73475cb40a568.nwd +++ b/tests/reference/guiMerge_73475cb40a568.nwd @@ -1,7 +1,6 @@ %%~name: Chapter One %%~path: b3643d0f92e32/73475cb40a568 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:39:07 ## Chapter One @pov: Bod diff --git a/tests/reference/guiSplit_031b4af5197ec.nwd b/tests/reference/guiSplit_031b4af5197ec.nwd index bd819978..cbaf3205 100644 --- a/tests/reference/guiSplit_031b4af5197ec.nwd +++ b/tests/reference/guiSplit_031b4af5197ec.nwd @@ -1,7 +1,6 @@ %%~name: Scene One %%~path: 0e17daca5f3e1/031b4af5197ec %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:42:30 ### Scene One @pov: Bod diff --git a/tests/reference/guiSplit_25fc0e7096fc6.nwd b/tests/reference/guiSplit_25fc0e7096fc6.nwd index e6d495ea..3247412b 100644 --- a/tests/reference/guiSplit_25fc0e7096fc6.nwd +++ b/tests/reference/guiSplit_25fc0e7096fc6.nwd @@ -1,7 +1,6 @@ %%~name: Chapter One %%~path: 811786ad1ae74/25fc0e7096fc6 %%~kind: NOVEL/CHAPTER -%%~time: 2021-01-30 18:40:32 ## Chapter One @pov: Bod diff --git a/tests/reference/guiSplit_2858dcd1057d3.nwd b/tests/reference/guiSplit_2858dcd1057d3.nwd index 64292037..c1e79bdf 100644 --- a/tests/reference/guiSplit_2858dcd1057d3.nwd +++ b/tests/reference/guiSplit_2858dcd1057d3.nwd @@ -1,7 +1,6 @@ %%~name: Scene Two %%~path: 0e17daca5f3e1/2858dcd1057d3 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:43:17 ### Scene Two @pov: Bod diff --git a/tests/reference/guiSplit_2fca346db6561.nwd b/tests/reference/guiSplit_2fca346db6561.nwd index f818dc02..c33e7901 100644 --- a/tests/reference/guiSplit_2fca346db6561.nwd +++ b/tests/reference/guiSplit_2fca346db6561.nwd @@ -1,7 +1,6 @@ %%~name: Scene Two, Section Two %%~path: 0e17daca5f3e1/2fca346db6561 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:43:30 #### Scene Two, Section Two Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. diff --git a/tests/reference/guiSplit_31489056e0916.nwd b/tests/reference/guiSplit_31489056e0916.nwd index 9d770c6c..e494f6b9 100644 --- a/tests/reference/guiSplit_31489056e0916.nwd +++ b/tests/reference/guiSplit_31489056e0916.nwd @@ -1,7 +1,6 @@ %%~name: Scene One %%~path: 811786ad1ae74/31489056e0916 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:41:22 ### Scene One @pov: Bod diff --git a/tests/reference/guiSplit_41cfc0d1f2d12.nwd b/tests/reference/guiSplit_41cfc0d1f2d12.nwd index 1c91f150..6f887f44 100644 --- a/tests/reference/guiSplit_41cfc0d1f2d12.nwd +++ b/tests/reference/guiSplit_41cfc0d1f2d12.nwd @@ -1,7 +1,6 @@ %%~name: Scene One, Section Two %%~path: 0e17daca5f3e1/41cfc0d1f2d12 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:43:04 #### Scene One, Section Two Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. diff --git a/tests/reference/guiSplit_98010bd9270f9.nwd b/tests/reference/guiSplit_98010bd9270f9.nwd index 451a4bb5..0725f6a5 100644 --- a/tests/reference/guiSplit_98010bd9270f9.nwd +++ b/tests/reference/guiSplit_98010bd9270f9.nwd @@ -1,7 +1,6 @@ %%~name: Scene Two %%~path: 811786ad1ae74/98010bd9270f9 %%~kind: NOVEL/SCENE -%%~time: 2021-01-30 18:41:48 ### Scene Two @pov: Bod diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index a84e6972..bbfe658b 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -28,7 +28,6 @@ from dummy import causeOSError from nw.core import NWProject, NWDoc from nw.core.item import NWItem from nw.constants import nwItemClass, nwItemLayout -from nw.common import formatTimeStamp @pytest.mark.core def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): @@ -75,7 +74,6 @@ def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): assert theDoc.saveDocument(theText) # Save again to ensure temp file and previous file is handled - monkeypatch.setattr("nw.core.document.time", lambda: 123.4) assert theDoc.saveDocument(theText) # Check file content @@ -85,11 +83,9 @@ def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): "%%~name: New File\n" f"%%~path: a508bb932959c/{xHandle}\n" "%%~kind: NOVEL/SCENE\n" - f"%%~time: {formatTimeStamp(123.4)}\n" "### Test File\n\n" "Text ...\n\n" ) - monkeypatch.undo() # Force no meta data theDoc._theItem = None @@ -151,14 +147,12 @@ def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal): assert theLayout == nwItemLayout.SCENE # Add meta data garbage - monkeypatch.setattr("nw.core.document.time", lambda: 123.4) assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n") with open(docPath, mode="r", encoding="utf8") as inFile: assert inFile.read() == ( "%%~name: New Scene\n" f"%%~path: a6d311a93600a/{sHandle}\n" "%%~kind: NOVEL/SCENE\n" - f"%%~time: {formatTimeStamp(123.4)}\n" "%%~ stuff\n" "### Test File\n\n" "Text ...\n\n" diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 94931ce5..6d0034c7 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -28,9 +28,8 @@ from tools import cmpFiles from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox -from nw.gui.itemeditor import GuiItemEditor from nw.constants import nwItemType, nwDocAction keyDelay = 2 @@ -43,9 +42,6 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) - monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None) - monkeypatch.setattr(GuiItemEditor, "result", lambda *args: QDialog.Accepted) # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) @@ -325,25 +321,25 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_031b4af5197ec.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(fncProj, "content", "1a6562590ef19.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_1a6562590ef19.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_1a6562590ef19.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(fncProj, "content", "0e17daca5f3e1.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(fncProj, "content", "41cfc0d1f2d12.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd") compFile = os.path.join(refDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) # qtbot.stopForInteraction() diff --git a/tests/test_gui/test_gui_mergesplit.py b/tests/test_gui/test_gui_mergesplit.py index d10c2394..b2d0135e 100644 --- a/tests/test_gui/test_gui_mergesplit.py +++ b/tests/test_gui/test_gui_mergesplit.py @@ -66,7 +66,7 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiMerge_73475cb40a568.nwd") compFile = os.path.join(refDir, "guiMerge_73475cb40a568.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) # Split By Chapter assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -92,7 +92,7 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiMerge_71ee45a3c0db9.nwd") compFile = os.path.join(refDir, "guiMerge_73475cb40a568.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [1, 2, 3, 4]) + assert cmpFiles(testFile, compFile, [1, 2, 3]) # Split By Scene assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -116,19 +116,19 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiSplit_25fc0e7096fc6.nwd") compFile = os.path.join(refDir, "guiSplit_25fc0e7096fc6.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd") testFile = os.path.join(outDir, "guiSplit_31489056e0916.nwd") compFile = os.path.join(refDir, "guiSplit_31489056e0916.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd") testFile = os.path.join(outDir, "guiSplit_98010bd9270f9.nwd") compFile = os.path.join(refDir, "guiSplit_98010bd9270f9.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) # Split By Section assert nwGUI.treeView.setSelectedHandle("73475cb40a568") @@ -154,31 +154,31 @@ def testGuiMergeSplit_Tools(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir) testFile = os.path.join(outDir, "guiSplit_1a6562590ef19.nwd") compFile = os.path.join(refDir, "guiSplit_25fc0e7096fc6.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [1, 2, 3, 4]) + assert cmpFiles(testFile, compFile, [1, 2, 3]) projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd") testFile = os.path.join(outDir, "guiSplit_031b4af5197ec.nwd") compFile = os.path.join(refDir, "guiSplit_031b4af5197ec.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd") testFile = os.path.join(outDir, "guiSplit_41cfc0d1f2d12.nwd") compFile = os.path.join(refDir, "guiSplit_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd") testFile = os.path.join(outDir, "guiSplit_2858dcd1057d3.nwd") compFile = os.path.join(refDir, "guiSplit_2858dcd1057d3.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd") testFile = os.path.join(outDir, "guiSplit_2fca346db6561.nwd") compFile = os.path.join(refDir, "guiSplit_2fca346db6561.nwd") copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [4]) + assert cmpFiles(testFile, compFile) # qtbot.stopForInteraction() From 013fb58267101f2f87f389f43ac7924d2921c37f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 12:50:42 +0100 Subject: [PATCH 040/104] Block document action if editor does not have focus --- nw/gui/doceditor.py | 4 ++++ tests/test_gui/test_gui_doceditor.py | 3 +++ tests/test_gui/test_gui_mainmenu.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index bc83719b..e4a25e4e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -646,6 +646,10 @@ class GuiDocEditor(QTextEdit): this class when calling these actions from other classes. """ logger.verbose("Requesting action: %s" % theAction.name) + if not self.hasFocus(): + logger.verbose("Editor does not have focus") + return False + if self.theHandle is None: logger.error("No document open") return False diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index ba435ad0..cb9f35d1 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -30,6 +30,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor from PyQt5.QtWidgets import QAction, QMessageBox +from nw.gui.doceditor import GuiDocEditor from nw.gui.projtree import GuiProjectTree from nw.constants import nwItemType, nwDocAction @@ -44,6 +45,7 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) + monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) @@ -353,6 +355,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index dcd32e91..f6504abf 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor, QTextBlock from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox +from nw.gui.doceditor import GuiDocEditor from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwKeyWords keyDelay = 2 @@ -39,6 +40,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) # Test Document Action with No Project assert not nwGUI.docEditor.docAction(nwDocAction.COPY) From ddda934eedd1ce4c942ac0576692a25700bd46f7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 12:56:58 +0100 Subject: [PATCH 041/104] Fix tests --- tests/test_gui/test_gui_doceditor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index cb9f35d1..b169e457 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -28,8 +28,9 @@ from tools import cmpFiles from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from nw.gui.itemeditor import GuiItemEditor from nw.gui.doceditor import GuiDocEditor from nw.gui.projtree import GuiProjectTree from nw.constants import nwItemType, nwDocAction @@ -44,6 +45,9 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None) + monkeypatch.setattr(GuiItemEditor, "result", lambda *args: QDialog.Accepted) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) From cca895e4184cfbb38e06ac2cec5cad63139f1461 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 13:35:40 +0100 Subject: [PATCH 042/104] Cleanup in main menu and move the Move Up/Down entries to the project menu --- nw/gui/mainmenu.py | 54 +++++++++++++++++++++------------------------- nw/gui/projtree.py | 2 +- nw/guimain.py | 4 ++-- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 1bdd1c32..2923938b 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -115,22 +115,25 @@ class GuiMainMenu(QMenuBar): ## def setSpellCheck(self, theMode): - """Set the spell check check box to theMode. This is controlled - by the document editor class, which holds the master spell check - flag. + """Forward spell check check state to its action. """ self.aSpellCheck.setChecked(theMode) return def setAutoOutline(self, theMode): - """Set the auto outline check box to theMode. Used during - initialisation. + """Forward auto outline check state to its action. """ self.aAutoOutline.setChecked(theMode) return + def setFocusMode(self, theMode): + """Forward focus mode check state to its action. + """ + self.aFocusMode.setChecked(theMode) + return + ## - # Menu Action + # Slots ## def _toggleSpellCheck(self, isChecked=False): @@ -164,17 +167,11 @@ class GuiMainMenu(QMenuBar): return True def _openWebsite(self, theUrl): - """Open an URL in the system's default browser. + """Open a URL in the system's default browser. """ QDesktopServices.openUrl(QUrl(theUrl)) return True - def _openIssue(self): - """Open the issue tracker URL in the system's default browser. - """ - QDesktopServices.openUrl(QUrl(nw.__issuesurl__)) - return True - ## # Menu Builders ## @@ -276,6 +273,20 @@ class GuiMainMenu(QMenuBar): self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) + # Project > Move Up + self.aMoveUp = QAction("Move Item Up", self) + self.aMoveUp.setStatusTip("Move project item up") + self.aMoveUp.setShortcut("Ctrl+Up") + self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) + self.projMenu.addAction(self.aMoveUp) + + # Project > Move Down + self.aMoveDown = QAction("Move Item Down", self) + self.aMoveDown.setStatusTip("Move project item down") + self.aMoveDown.setShortcut("Ctrl+Down") + self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) + self.projMenu.addAction(self.aMoveDown) + # Project > Empty Trash self.aEmptyTrash = QAction("Empty Trash", self) self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder") @@ -891,23 +902,6 @@ class GuiMainMenu(QMenuBar): # Tools self.toolsMenu = self.addMenu("&Tools") - # Tools > Move Up - self.aMoveUp = QAction("Move Tree Item Up", self) - self.aMoveUp.setStatusTip("Move item up") - self.aMoveUp.setShortcut("Ctrl+Shift+Up") - self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) - self.toolsMenu.addAction(self.aMoveUp) - - # Tools > Move Down - self.aMoveDown = QAction("Move Tree Item Down", self) - self.aMoveDown.setStatusTip("Move item down") - self.aMoveDown.setShortcut("Ctrl+Shift+Down") - self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) - self.toolsMenu.addAction(self.aMoveDown) - - # Tools > Separator - self.toolsMenu.addSeparator() - # Tools > Toggle Spell Check self.aSpellCheck = QAction("Check Spelling", self) self.aSpellCheck.setStatusTip("Toggle check spelling") diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 31cdb19a..8d81a063 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -335,7 +335,7 @@ class GuiProjectTree(QTreeWidget): logger.error("No project open") return False - if qApp.focusWidget() != self: + if not self.hasFocus(): return False tHandle = self.getSelectedHandle() diff --git a/nw/guimain.py b/nw/guimain.py index f6538bf0..eff2f3d3 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -1167,11 +1167,11 @@ class GuiMain(QMainWindow): """ if self.docEditor.theHandle is None: logger.error("No document open, so not activating Focus Mode") - self.mainMenu.aFocusMode.setChecked(self.isFocusMode) + self.mainMenu.setFocusMode(self.isFocusMode) return False self.isFocusMode = not self.isFocusMode - self.mainMenu.aFocusMode.setChecked(self.isFocusMode) + self.mainMenu.setFocusMode(self.isFocusMode) if self.isFocusMode: logger.debug("Activating Focus Mode") self.mainTabs.setCurrentWidget(self.splitDocs) From 3cecc1229f1c7afd92dda721d1dda0e2f3cd99b4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 13:44:44 +0100 Subject: [PATCH 043/104] Fix GUI blocking in tests --- tests/test_gui/test_gui_docviewer.py | 1 + tests/test_gui/test_gui_outline.py | 1 + tests/test_gui/test_gui_projsettings.py | 1 + tests/test_gui/test_gui_projtree.py | 7 +++++-- tests/test_gui/test_gui_projwizard.py | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 8c928a97..3585c8c8 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -38,6 +38,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) # Open project nwGUI.theProject.projTree.setSeed(42) diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 125e59b4..bfeb2f96 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -37,6 +37,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) assert nwGUI.openProject(nwLipsum) nwGUI.mainConf.lastPath = nwLipsum diff --git a/tests/test_gui/test_gui_projsettings.py b/tests/test_gui/test_gui_projsettings.py index 50a5fff1..9955aac8 100644 --- a/tests/test_gui/test_gui_projsettings.py +++ b/tests/test_gui/test_gui_projsettings.py @@ -48,6 +48,7 @@ def testGuiProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) # Check that we cannot open when there is no project nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 317b8c58..c750537c 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -38,6 +38,9 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "warning", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) monkeypatch.setattr(GuiMain, "editItem", lambda *args: None) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) @@ -96,12 +99,12 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): nwTree.setSelectedHandle("8c659a11cd429") # Shift focus and try to move item - monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: None) + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: False) assert not nwTree.moveTreeItem(1) assert nwTree.getTreeFromHandle("a6d311a93600a") == [ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" ] - monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: nwTree) + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) # Move second item up twice (should give same result) nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) diff --git a/tests/test_gui/test_gui_projwizard.py b/tests/test_gui/test_gui_projwizard.py index f2a967fc..4208bc8f 100644 --- a/tests/test_gui/test_gui_projwizard.py +++ b/tests/test_gui/test_gui_projwizard.py @@ -46,6 +46,7 @@ def testGuiProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) if sys.platform.startswith("darwin"): # Disable for macOS because the test segfaults on QWizard.show() From 2307dfaaac19766b0e1b54eeefab33aea166955c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 16:54:49 +0100 Subject: [PATCH 044/104] Add feature to undo last project tree move --- docs/source/int_interface.rst | 6 +- nw/constants/constants.py | 3 + nw/gui/mainmenu.py | 15 +++-- nw/gui/projtree.py | 105 +++++++++++++++++++++++++++++----- 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/docs/source/int_interface.rst b/docs/source/int_interface.rst index cf6bc065..f5b8d68d 100644 --- a/docs/source/int_interface.rst +++ b/docs/source/int_interface.rst @@ -378,6 +378,8 @@ Most features are available as keyboard shortcuts. These are as follows: ":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." ":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline." ":kbd:`Ctrl`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder." + ":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree." + ":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree." ":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes." ":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes." ":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer." @@ -392,9 +394,7 @@ Most features are available as keyboard shortcuts. These are as follows: ":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Alternative sequence for redo last undo." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Up`", "Move item one step up in the project tree." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Down`", "Move item one step down in the project tree." + ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item." ":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website." ":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)" ":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)" diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 5b961e10..26bb8f7d 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -56,6 +56,9 @@ class nwLists(): # Item classes where the full list of novel layouts are allowed CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE} + # Item classes which do not require items to have same class + FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH} + # END Class nwLists class nwRegEx(): diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 2923938b..38efa1d0 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -260,15 +260,15 @@ class GuiMainMenu(QMenuBar): self.projMenu.addSeparator() # Project > Edit - self.aEditItem = QAction("Edit Project Item", self) - self.aEditItem.setStatusTip("Change item settings") + self.aEditItem = QAction("Edit Item", self) + self.aEditItem.setStatusTip("Change project item settings") self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None)) self.projMenu.addAction(self.aEditItem) # Project > Delete - self.aDeleteItem = QAction("Delete Project Item", self) - self.aDeleteItem.setStatusTip("Delete selected item") + self.aDeleteItem = QAction("Delete Item", self) + self.aDeleteItem.setStatusTip("Delete selected project item") self.aDeleteItem.setShortcut("Ctrl+Del") self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) @@ -287,6 +287,13 @@ class GuiMainMenu(QMenuBar): self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) self.projMenu.addAction(self.aMoveDown) + # Project > Undo Last Action + self.aMoveUndo = QAction("Undo Last Move", self) + self.aMoveUndo.setStatusTip("Undo last item move") + self.aMoveUndo.setShortcut("Ctrl+Shift+Z") + self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove()) + self.projMenu.addAction(self.aMoveUndo) + # Project > Empty Trash self.aEmptyTrash = QAction("Empty Trash", self) self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder") diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 8d81a063..4b3aeb7e 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -33,12 +33,12 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction + QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction ) from nw.core import NWDoc from nw.constants import ( - nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst + nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists ) logger = logging.getLogger(__name__) @@ -68,6 +68,7 @@ class GuiProjectTree(QTreeWidget): self._treeMap = {} self._treeChanged = False self._timeChanged = 0 + self._lastMove = {} ## # Build GUI @@ -361,6 +362,7 @@ class GuiProjectTree(QTreeWidget): return False cItem = pItem.takeChild(tIndex) pItem.insertChild(nIndex, cItem) + self._recordLastMove(cItem, pItem, tIndex) self.clearSelection() cItem.setSelected(True) @@ -517,8 +519,9 @@ class GuiProjectTree(QTreeWidget): theDoc = NWDoc(self.theProject, self.theParent) theDoc.deleteDocument(tHandle) - del self.theProject.projTree[tHandle] self.theIndex.deleteHandle(tHandle) + self._deleteTreeItem(tHandle) + self._setTreeChanged(True) else: # The file is not already in the trash folder, so we @@ -541,11 +544,12 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - nwItemS.setParent(self.theProject.projTree.trashRoot()) + self._updateItemParent(tHandle) self.propagateCount(tHandle, wCount) - self._setTreeChanged(True) self.theIndex.deleteHandle(tHandle) + self._recordLastMove(trItemS, trItemP, tIndex) + self._setTreeChanged(True) elif nwItemS.itemType == nwItemType.FOLDER: logger.debug("User requested folder %s deleted" % tHandle) @@ -556,7 +560,8 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) if trItemS.childCount() == 0: trItemP.takeChild(tIndex) - del self.theProject.projTree[tHandle] + self._deleteTreeItem(tHandle) + self._setTreeChanged(True) else: self.makeAlert(( "Cannot delete folder. It is not empty. " @@ -570,7 +575,7 @@ class GuiProjectTree(QTreeWidget): tIndex = self.indexOfTopLevelItem(trItemS) if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) - del self.theProject.projTree[tHandle] + self._deleteTreeItem(tHandle) self.theParent.mainMenu.setAvailableRoot() self._setTreeChanged(True) else: @@ -679,6 +684,59 @@ class GuiProjectTree(QTreeWidget): logger.debug("%d items added to the project tree" % iCount) return True + def undoLastMove(self): + """Attempt to undo the last action. + """ + srcItem = self._lastMove.get("item", None) + dstItem = self._lastMove.get("parent", None) + dstIndex = self._lastMove.get("index", None) + + if not self.hasFocus(): + return False + + if srcItem is None or dstItem is None or dstIndex is None: + logger.verbose("No tree move to undo") + return False + + if srcItem not in self._treeMap.values(): + logger.warning("Source item no longer exists") + return False + + if dstItem not in self._treeMap.values(): + logger.warning("Previous parent item no longer exists") + return False + + dstIndex = min(max(0, dstIndex), dstItem.childCount()) + wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole)) + sHandle = srcItem.data(self.C_NAME, Qt.UserRole) + dHandle = dstItem.data(self.C_NAME, Qt.UserRole) + logger.debug("Moving item %s back to %s, index %d" % ( + sHandle, dHandle, dstIndex + )) + + self.propagateCount(sHandle, 0) + parItem = srcItem.parent() + srcIndex = parItem.indexOfChild(srcItem) + movItem = parItem.takeChild(srcIndex) + dstItem.insertChild(dstIndex, movItem) + self._updateItemParent(sHandle) + self.propagateCount(sHandle, wCount) + + snItem = self.theProject.projTree[sHandle] + dnItem = self.theProject.projTree[dHandle] + if dnItem.itemClass not in nwLists.FREE_CLASS: + logger.debug("Item %s class has been changed from %s to %s" % ( + sHandle, snItem.itemClass.name, dnItem.itemClass.name + )) + snItem.setClass(dnItem.itemClass) + self.setTreeItemValues(sHandle) + + self.clearSelection() + movItem.setSelected(True) + self._lastMove = {} + + return True + def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. @@ -779,6 +837,7 @@ class GuiProjectTree(QTreeWidget): return sItem = self._getTreeItem(sHandle) + pItem = sItem.parent() dItem = self.itemFromIndex(dIndex) dHandle = dItem.data(self.C_NAME, Qt.UserRole) snItem = self.theProject.projTree[sHandle] @@ -791,11 +850,10 @@ class GuiProjectTree(QTreeWidget): isSame = snItem.itemClass == dnItem.itemClass isNone = snItem.itemClass == nwItemClass.NO_CLASS isNote = snItem.itemLayout == nwItemLayout.NOTE - onFile = dnItem.itemType == nwItemType.FILE isRoot = snItem.itemType == nwItemType.ROOT - onFree = dnItem.itemClass == nwItemClass.ARCHIVE - onFree |= dnItem.itemClass == nwItemClass.TRASH - onFree &= snItem.itemType == nwItemType.FILE + isFile = snItem.itemType == nwItemType.FILE + onFile = dnItem.itemType == nwItemType.FILE + onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot: logger.debug("Drag'n'drop of item %s accepted" % sHandle) @@ -807,14 +865,13 @@ class GuiProjectTree(QTreeWidget): # and the target is not a free root folder, update its class if not (isSame or onFree): logger.debug("Item %s class has been changed from %s to %s" % ( - sHandle, - snItem.itemClass.name, - dnItem.itemClass.name + sHandle, snItem.itemClass.name, dnItem.itemClass.name )) snItem.setClass(dnItem.itemClass) self.setTreeItemValues(sHandle) self.propagateCount(sHandle, wCount) + self._recordLastMove(sItem, pItem, pItem.indexOfChild(sItem)) # The items dropped into archive or trash should be removed # from the project index, for all other items, we rescan the @@ -844,6 +901,13 @@ class GuiProjectTree(QTreeWidget): """ return self._treeMap.get(tHandle, None) + def _deleteTreeItem(self, tHandle): + """Delete a tree item from the project and the map. + """ + del self.theProject.projTree[tHandle] + self._treeMap.pop(tHandle, None) + return + def _scanChildren(self, theList, theItem, theIndex): """This is a recursive function returning all items in a tree starting at a given QTreeWidgetItem. @@ -984,6 +1048,19 @@ class GuiProjectTree(QTreeWidget): return + def _recordLastMove(self, srcItem, parItem, parIndex): + """Record the last action so that it can be undone. + """ + prevItem = self._lastMove.get("item", None) + if prevItem is None or srcItem != prevItem: + self._lastMove = { + "item": srcItem, + "parent": parItem, + "index": parIndex, + } + + return + # END Class GuiProjectTree class GuiProjectTreeMenu(QMenu): From a52b71ef43cea607d69b21f60eaa209a502a3f30 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 22:06:13 +0100 Subject: [PATCH 045/104] Remove ToC.txt files from repo --- .gitignore | 1 + nw/constants/constants.py | 1 - sample/ToC.txt | 23 ----------------------- tests/lipsum/ToC.txt | 22 ---------------------- tests/minimal/ToC.txt | 10 ---------- 5 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 sample/ToC.txt delete mode 100644 tests/lipsum/ToC.txt delete mode 100644 tests/minimal/ToC.txt diff --git a/.gitignore b/.gitignore index 66c21d61..3d1b337e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ __pycache__ /sample/meta *.bak *.lock +ToC.txt # PyTest /prof/ diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 26bb8f7d..e748c0ff 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -49,7 +49,6 @@ class nwConst(): class nwLists(): """Lists used for grouping various other constants. """ - # Regular user-accessible item types REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE} diff --git a/sample/ToC.txt b/sample/ToC.txt deleted file mode 100644 index b106e892..00000000 --- a/sample/ToC.txt +++ /dev/null @@ -1,23 +0,0 @@ - -Table of Contents -================= - -File Name Class Layout Document Label ---------------------------------------------------------------------- -content/53b69b83cdafc.nwd NOVEL TITLE Title Page -content/974e400180a99.nwd NOVEL PAGE Page -content/edca4be2fcaf8.nwd NOVEL PARTITION Part One -content/6a2d6d5f4f401.nwd NOVEL CHAPTER Chapter One -content/636b6aa9b697b.nwd NOVEL SCENE Making a Scene -content/bc0cbd2a407f3.nwd NOVEL SCENE Another Scene -content/ba8a28a246524.nwd NOVEL UNNUMBERED Interlude -content/96b68994dfa3d.nwd NOVEL NOTE A Note on Structure -content/88706ddc78b1b.nwd NOVEL CHAPTER Chapter Two -content/ae7339df26ded.nwd NOVEL SCENE We Found John! -content/14298de4d9524.nwd CHARACTER NOTE John Smith -content/bb2c23b3c42cc.nwd CHARACTER NOTE Jane Smith -content/b3e74dbc1f584.nwd WORLD NOTE Earth -content/f1471bef9f2ae.nwd WORLD NOTE Space -content/5eaea4e8cdee8.nwd WORLD NOTE Mars -content/8a5deb88c0e97.nwd NOVEL SCENE Old File -content/b8136a5a774a0.nwd NOVEL SCENE Delete Me! diff --git a/tests/lipsum/ToC.txt b/tests/lipsum/ToC.txt deleted file mode 100644 index a499c6f8..00000000 --- a/tests/lipsum/ToC.txt +++ /dev/null @@ -1,22 +0,0 @@ - - Table of Contents -=================== - - File Name Class Document Label --------------------------------------------------------------------------------- - content/04468803b92e1.nwd WORLD Ancient Europe - content/2426c6f0ca922.nwd PLOT Main - content/441420a886d82.nwd NOVEL Chapter Two - content/47666c91c7ccf.nwd NOVEL Scene Five - content/4c4f28287af27.nwd CHARACTER Mr. Nobody - content/7a992350f3eb6.nwd NOVEL Lorem Ipsum - content/846352075de7d.nwd NOVEL Interlude - content/88243afbe5ed8.nwd NOVEL Scene One - content/88d59a277361b.nwd NOVEL Prologue - content/8c58a65414c23.nwd NOVEL Front Matter - content/db7e733775d4d.nwd NOVEL Act One - content/eb103bc70c90c.nwd NOVEL Scene Three - content/f8c0562e50f1b.nwd NOVEL Scene Four - content/f96ec11c6a3da.nwd NOVEL Scene Two - content/fb609cd8319dc.nwd NOVEL Chapter One - diff --git a/tests/minimal/ToC.txt b/tests/minimal/ToC.txt deleted file mode 100644 index f5b94b72..00000000 --- a/tests/minimal/ToC.txt +++ /dev/null @@ -1,10 +0,0 @@ - - Table of Contents -=================== - - File Name Class Document Label --------------------------------------------------------------------------------- - content/8c659a11cd429.nwd NOVEL New Scene - content/a35baf2e93843.nwd NOVEL Title Page - content/f5ab3e30151e1.nwd NOVEL New Chapter - From 9596d24cca33ed888d4e29b2a71a689827530b20 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 22:29:39 +0100 Subject: [PATCH 046/104] Some final tweaks to the drag and drop feature --- nw/gui/projtree.py | 22 +++++++++++++++------- sample/nwProject.nwx | 8 ++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 4b3aeb7e..3573eaef 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -837,7 +837,6 @@ class GuiProjectTree(QTreeWidget): return sItem = self._getTreeItem(sHandle) - pItem = sItem.parent() dItem = self.itemFromIndex(dIndex) dHandle = dItem.data(self.C_NAME, Qt.UserRole) snItem = self.theProject.projTree[sHandle] @@ -846,16 +845,25 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) return + pItem = sItem.parent() + pIndex = 0 + if pItem is not None: + pIndex = pItem.indexOfChild(sItem) + wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) + isFile = snItem.itemType == nwItemType.FILE + isRoot = snItem.itemType == nwItemType.ROOT + onFile = dnItem.itemType == nwItemType.FILE + isSame = snItem.itemClass == dnItem.itemClass isNone = snItem.itemClass == nwItemClass.NO_CLASS isNote = snItem.itemLayout == nwItemLayout.NOTE - isRoot = snItem.itemType == nwItemType.ROOT - isFile = snItem.itemType == nwItemType.FILE - onFile = dnItem.itemType == nwItemType.FILE onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile - isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem - if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot: + + allowDrop = isSame or isNone or isNote or onFree + allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) + + if allowDrop and not isRoot: logger.debug("Drag'n'drop of item %s accepted" % sHandle) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) @@ -871,7 +879,7 @@ class GuiProjectTree(QTreeWidget): self.setTreeItemValues(sHandle) self.propagateCount(sHandle, wCount) - self._recordLastMove(sItem, pItem, pItem.indexOfChild(sItem)) + self._recordLastMove(sItem, pItem, pIndex) # The items dropped into archive or trash should be removed # from the project index, for all other items, we rescan the diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 69bd2eee..cbb940be 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 874 + 881 157 - 42533 + 42668 False @@ -117,7 +117,7 @@ 1st Draft True SCENE - 1811 + 1810 318 8 1880 From 0c4d278de2ad3b552cb73bd9c0a79d01524ca2d3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 22:53:58 +0100 Subject: [PATCH 047/104] Extract duplicate code into a new function --- nw/gui/projtree.py | 84 ++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 3573eaef..a2e7c7b0 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -298,10 +298,10 @@ class GuiProjectTree(QTreeWidget): # Save the text and index it newDoc.saveDocument(newText) - self.theParent.theIndex.scanText(tHandle, newText) + self.theIndex.scanText(tHandle, newText) # Get Word Counts - cC, wC, pC = self.theParent.theIndex.getCounts(tHandle) + cC, wC, pC = self.theIndex.getCounts(tHandle) nwItem.setCharCount(cC) nwItem.setWordCount(wC) nwItem.setParaCount(pC) @@ -719,17 +719,10 @@ class GuiProjectTree(QTreeWidget): srcIndex = parItem.indexOfChild(srcItem) movItem = parItem.takeChild(srcIndex) dstItem.insertChild(dstIndex, movItem) - self._updateItemParent(sHandle) - self.propagateCount(sHandle, wCount) snItem = self.theProject.projTree[sHandle] dnItem = self.theProject.projTree[dHandle] - if dnItem.itemClass not in nwLists.FREE_CLASS: - logger.debug("Item %s class has been changed from %s to %s" % ( - sHandle, snItem.itemClass.name, dnItem.itemClass.name - )) - snItem.setClass(dnItem.itemClass) - self.setTreeItemValues(sHandle) + self._postItemMove(sHandle, snItem, dnItem, wCount) self.clearSelection() movItem.setSelected(True) @@ -867,32 +860,9 @@ class GuiProjectTree(QTreeWidget): logger.debug("Drag'n'drop of item %s accepted" % sHandle) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) - self._updateItemParent(sHandle) - - # If the item does not have the same class as the target, - # and the target is not a free root folder, update its class - if not (isSame or onFree): - logger.debug("Item %s class has been changed from %s to %s" % ( - sHandle, snItem.itemClass.name, dnItem.itemClass.name - )) - snItem.setClass(dnItem.itemClass) - self.setTreeItemValues(sHandle) - - self.propagateCount(sHandle, wCount) + self._postItemMove(sHandle, snItem, dnItem, wCount) self._recordLastMove(sItem, pItem, pIndex) - # The items dropped into archive or trash should be removed - # from the project index, for all other items, we rescan the - # file to ensure the index is up to date. - if onFree: - self.theIndex.deleteHandle(sHandle) - else: - self.theIndex.reIndexHandle(sHandle) - - # Trigger dependent updates - self._setTreeChanged(True) - self._emitItemChange(sHandle) - else: theEvent.ignore() logger.debug("Drag'n'drop of item %s not accepted" % sHandle) @@ -904,6 +874,40 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## + def _postItemMove(self, sHandle, snItem, dnItem, wCount): + """Run various maintenance tasks for a moved item. + """ + isFile = snItem.itemType == nwItemType.FILE + isSame = snItem.itemClass == dnItem.itemClass + onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile + + self._updateItemParent(sHandle) + + # If the item does not have the same class as the target, + # and the target is not a free root folder, update its class + if not (isSame or onFree): + logger.debug("Item %s class has been changed from %s to %s" % ( + sHandle, snItem.itemClass.name, dnItem.itemClass.name + )) + snItem.setClass(dnItem.itemClass) + self.setTreeItemValues(sHandle) + + self.propagateCount(sHandle, wCount) + + # The items dropped into archive or trash should be removed + # from the project index, for all other items, we rescan the + # file to ensure the index is up to date. + if onFree: + self.theIndex.deleteHandle(sHandle) + else: + self.theIndex.reIndexHandle(sHandle) + + # Trigger dependent updates + self._setTreeChanged(True) + self._emitItemChange(sHandle) + + return + def _getTreeItem(self, tHandle): """Returns the QTreeWidgetItem of a given item handle. """ @@ -916,17 +920,17 @@ class GuiProjectTree(QTreeWidget): self._treeMap.pop(tHandle, None) return - def _scanChildren(self, theList, theItem, theIndex): + def _scanChildren(self, theList, tItem, tIndex): """This is a recursive function returning all items in a tree starting at a given QTreeWidgetItem. """ - tHandle = theItem.data(self.C_NAME, Qt.UserRole) + tHandle = tItem.data(self.C_NAME, Qt.UserRole) nwItem = self.theProject.projTree[tHandle] - nwItem.setExpanded(theItem.isExpanded()) - nwItem.setOrder(theIndex) + nwItem.setExpanded(tItem.isExpanded()) + nwItem.setOrder(tIndex) theList.append(tHandle) - for i in range(theItem.childCount()): - self._scanChildren(theList, theItem.child(i), i) + for i in range(tItem.childCount()): + self._scanChildren(theList, tItem.child(i), i) return theList def _addTreeItem(self, nwItem, nHandle=None): From af2fcbc059380d9be71dd5a34e316612bba4e752 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Jan 2021 23:01:36 +0100 Subject: [PATCH 048/104] Add a simple test for now --- tests/test_gui/test_gui_projtree.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index c750537c..b0fdaa89 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -134,6 +134,15 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" ] + # Move up twice, and undo + nwTree._lastMove = {} + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" + ] + # Move a root item (top level items are different) twice nwTree.flushTreeOrder() assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10 From cada78ca4d08c27760b5acf3b19c2ab6561cf2ab Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 1 Feb 2021 23:19:24 +0100 Subject: [PATCH 049/104] use what was added in #637 to replace first title lookup in editor saveText --- nw/gui/doceditor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index a6b6f3ed..e5930690 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -416,17 +416,22 @@ class GuiDocEditor(QTextEdit): self.theIndex.scanText(tHandle, docText) - hLevel, _ = self.theParent.theIndex.getFirstTitle(tHandle) + if self._updateHeaders(checkLevel=True): + if self.theParent.projTabs.currentIndex() == self.theParent.idxNovelView: + logger.verbose("Document headers have changed, updating novel tree") + self.theParent.novelView.refreshTree() + else: + self.theParent.novelView.updateWordCounts(tHandle) + + hLevel = "H0" + if self.theHeaders: + hLevel = self.theHeaders[0][1] + if self.theProject.projTree.updateItemLayout(tHandle, hLevel): self.theParent.treeView.setTreeItemValues(tHandle) self.nwDocument.saveDocument(docText) self.docFooter.updateInfo() - if self._updateHeaders(checkLevel=True): - self.theParent.novelView.refreshTree() - else: - self.theParent.novelView.updateWordCounts(tHandle) - return True def updateDocMargins(self): From 3baeef4f44a173a905b981e770446880d09b8a56 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 1 Feb 2021 23:20:07 +0100 Subject: [PATCH 050/104] Drop the whole firstTitle index again --- nw/core/index.py | 33 ---------- sample/nwProject.nwx | 10 +-- .../coreIndex_LoadSave_tagsIndex.json | 62 ------------------- tests/test_core/test_core_index.py | 53 ---------------- 4 files changed, 5 insertions(+), 153 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 6f72bf3b..ac1fba3f 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -59,7 +59,6 @@ class NWIndex(): self._novelIndex = {} self._noteIndex = {} self._textCounts = {} - self._firstTitle = {} # TimeStamps self._timeNovel = 0 @@ -80,7 +79,6 @@ class NWIndex(): self._novelIndex = {} self._noteIndex = {} self._textCounts = {} - self._firstTitle = {} self._timeNovel = 0 self._timeNotes = 0 self._timeIndex = 0 @@ -103,7 +101,6 @@ class NWIndex(): self._novelIndex.pop(tHandle, None) self._noteIndex.pop(tHandle, None) self._textCounts.pop(tHandle, None) - self._firstTitle.pop(tHandle, None) return @@ -172,7 +169,6 @@ class NWIndex(): self._novelIndex = theData.get("novelIndex", {}) self._noteIndex = theData.get("noteIndex", {}) self._textCounts = theData.get("textCounts", {}) - self._firstTitle = theData.get("firstTitle", {}) nowTime = round(time()) self._timeNovel = nowTime @@ -198,7 +194,6 @@ class NWIndex(): "novelIndex" : self._novelIndex, "noteIndex" : self._noteIndex, "textCounts" : self._textCounts, - "firstTitle" : self._firstTitle, }, outFile, indent=2) except Exception: logger.error("Failed to save index file") @@ -220,7 +215,6 @@ class NWIndex(): self._checkNovelNoteIndex("novelIndex") self._checkNovelNoteIndex("noteIndex") self._checkTextCounts() - self._checkFirstTitles() self.indexBroken = False except Exception: @@ -291,7 +285,6 @@ class NWIndex(): "tags" : [], "updated" : round(time()), } - self._firstTitle[tHandle] = ["H0", "T000000"] if itemLayout == nwItemLayout.NOTE: self._novelIndex.pop(tHandle, None) self._noteIndex[tHandle] = {} @@ -400,9 +393,6 @@ class NWIndex(): "updated" : round(time()), } - if self._firstTitle[tHandle][0] == "H0": - self._firstTitle[tHandle] = [hDepth, sTitle] - if hText != "": if isNovel: if tHandle in self._novelIndex: @@ -678,11 +668,6 @@ class NWIndex(): return theToC - def getFirstTitle(self, tHandle): - """Return the level and location of the first title of a handle. - """ - return self._firstTitle.get(tHandle, ["H0", "T000000"]) - def getCounts(self, tHandle, sTitle=None): """Returns the counts for a file, or a section of a file starting at title sTitle if it is provided. @@ -924,22 +909,4 @@ class NWIndex(): return - def _checkFirstTitles(self): - """Scan the first titles index for errors. - Waring: This function raises exceptions. - """ - for tHandle in self._firstTitle: - if not isHandle(tHandle): - raise KeyError("firstTitle key is not a handle") - - tEntry = self._firstTitle[tHandle] - if len(tEntry) != 2: - raise IndexError("firstTitle[a] expected 2 values") - if not tEntry[0] in self.H_VALID: - raise ValueError("firstTitle[a][0] is not a header level") - if not isTitleTag(tEntry[1]): - raise ValueError("firstTitle[a][1] is not a title tag") - - return - # END Class NWIndex diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index cbb940be..4bc8a637 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 881 - 157 - 42668 + 886 + 158 + 42826 False @@ -120,7 +120,7 @@ 1810 318 8 - 1880 + 219 Another Scene diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index 993b4e26..2aa3df3c 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -567,67 +567,5 @@ 259, 3 ] - }, - "firstTitle": { - "7a992350f3eb6": [ - "H1", - "T000001" - ], - "8c58a65414c23": [ - "H0", - "T000000" - ], - "88d59a277361b": [ - "H2", - "T000001" - ], - "db7e733775d4d": [ - "H1", - "T000001" - ], - "fb609cd8319dc": [ - "H2", - "T000001" - ], - "88243afbe5ed8": [ - "H3", - "T000001" - ], - "f96ec11c6a3da": [ - "H3", - "T000001" - ], - "846352075de7d": [ - "H2", - "T000001" - ], - "441420a886d82": [ - "H2", - "T000001" - ], - "eb103bc70c90c": [ - "H3", - "T000001" - ], - "f8c0562e50f1b": [ - "H3", - "T000001" - ], - "47666c91c7ccf": [ - "H3", - "T000001" - ], - "4c4f28287af27": [ - "H1", - "T000001" - ], - "2426c6f0ca922": [ - "H1", - "T000001" - ], - "04468803b92e1": [ - "H1", - "T000001" - ] } } \ No newline at end of file diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 8d4a2e89..9b1c656a 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -591,11 +591,6 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert wC == 16 assert pC == 2 - # getFirstTitle - # ============= - - assert theIndex.getFirstTitle(cHandle) == ["H1", "T000001"] - # Novel Stats # =========== @@ -1166,51 +1161,3 @@ def testCoreIndex_CheckTextCounts(dummyGUI): theIndex._checkTextCounts() # END Test testCoreIndex_CheckTextCounts - -@pytest.mark.core -def testCoreIndex_CheckFirstTitle(dummyGUI): - """Test the first title checker. - """ - theProject = NWProject(dummyGUI) - theIndex = NWIndex(theProject, dummyGUI) - - # Valid Index - theIndex._firstTitle = { - "53b69b83cdafc": ["H1", "T000001"], - "974e400180a99": ["H0", "T000000"], - } - assert theIndex._checkFirstTitles() is None - - # Invalid Handle - theIndex._firstTitle = { - "53b69b83cdafc": ["H1", "T000001"], - "h74e400180a99": ["H0", "T000000"], - } - with pytest.raises(KeyError): - theIndex._checkFirstTitles() - - # Wrong Length - theIndex._firstTitle = { - "53b69b83cdafc": ["H1", "T000001"], - "974e400180a99": ["H0", "T000000", "stuff"], - } - with pytest.raises(IndexError): - theIndex._checkFirstTitles() - - # Wrong Header - theIndex._firstTitle = { - "53b69b83cdafc": ["H1", "T000001"], - "974e400180a99": ["XX", "T000000"], - } - with pytest.raises(ValueError): - theIndex._checkFirstTitles() - - # Wrong Title - theIndex._firstTitle = { - "53b69b83cdafc": ["H1", "T000001"], - "974e400180a99": ["H0", "INVALID"], - } - with pytest.raises(ValueError): - theIndex._checkFirstTitles() - -# END Test testCoreIndex_CheckFirstTitle From c7a42a1641494a5a6399b686dc99a3b366d073a1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 15:07:08 +0100 Subject: [PATCH 051/104] Basic to markdown class added --- nw/core/tomd.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 nw/core/tomd.py diff --git a/nw/core/tomd.py b/nw/core/tomd.py new file mode 100644 index 00000000..35bcdf47 --- /dev/null +++ b/nw/core/tomd.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +""" +novelWriter – Markdown Text Converter +===================================== +Extends the Tokenizer class to generate Makrdown output + +File History: +Created: 2021-02-06 [1.2a0] + +This file is a part of novelWriter +Copyright 2018–2021, 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 . +""" + +import logging + +from nw.core.tokenizer import Tokenizer +from nw.constants import nwLabels + +logger = logging.getLogger(__name__) + +class ToMarkdown(Tokenizer): + + M_STD = 0 # Standard Markdown + M_GH = 1 # GitHub Markdown + M_NW = 2 # novelWriter Markdown + + def __init__(self, theProject, theParent): + Tokenizer.__init__(self, theProject, theParent) + + self.genMode = self.M_STD + self.fullMD = [] + + return + + ## + # Setters + ## + + def setStandardMarkdown(self): + self.genMode = self.M_STD + return + + def setGitHubMarkdown(self): + self.genMode = self.M_GH + return + + def setNovelWriterMarkdown(self): + self.genMode = self.M_MD + return + + ## + # Class Methods + ## + + def getFullResultSize(self): + """Return the size of the full Markdown result. + """ + return sum([len(x) for x in self.fullMD]) + + def doConvert(self): + """Convert the list of text tokens into a HTML document saved + to theResult. + """ + if self.genMode == self.M_STD: + # Standard + mdTags = { + self.FMT_B_B : "**", self.FMT_B_E : "**", + self.FMT_I_B : "_", self.FMT_I_E : "_", + self.FMT_D_B : "", self.FMT_D_E : "", + } + else: + # GitHub and novelWriter + mdTags = { + self.FMT_B_B : "**", self.FMT_B_E : "**", + self.FMT_I_B : "_", self.FMT_I_E : "_", + self.FMT_D_B : "~~", self.FMT_D_E : "~~", + } + + self.theResult = "" + + thisPar = [] + tmpResult = [] + + for tType, tLine, tText, tFormat, tStyle in self.theTokens: + + # Process Text Type + if tType == self.T_EMPTY: + if len(thisPar) > 0: + tTemp = "".join(thisPar) + tmpResult.append("%s\n" % tTemp) + + thisPar = [] + + elif tType == self.T_TITLE: + tHead = tText.replace(r"\\", "\n") + tmpResult.append("# %s\n" % tHead) + + elif tType == self.T_HEAD1: + tHead = tText.replace(r"\\", "\n") + tmpResult.append("# %s\n" % tHead) + + elif tType == self.T_HEAD2: + tHead = tText.replace(r"\\", "\n") + tmpResult.append("## %s\n" % tHead) + + elif tType == self.T_HEAD3: + tHead = tText.replace(r"\\", "\n") + tmpResult.append("### %s\n" % tHead) + + elif tType == self.T_HEAD4: + tHead = tText.replace(r"\\", "\n") + tmpResult.append("#### %s\n" % tHead) + + elif tType == self.T_SEP: + tmpResult.append("%s\n\n" % tText) + + elif tType == self.T_SKIP: + tmpResult.append("\n\n\n") + + elif tType == self.T_TEXT: + tTemp = tText + for xPos, xLen, xFmt in reversed(tFormat): + tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:] + if tText.endswith(" "): + thisPar.append(tTemp.rstrip() + " ") + else: + thisPar.append(tTemp.rstrip() + " ") + + elif tType == self.T_SYNOPSIS and self.doSynopsis: + tmpResult.append(self._formatSynopsis(tText)) + + elif tType == self.T_COMMENT and self.doComments: + tmpResult.append(self._formatComments(tText)) + + elif tType == self.T_KEYWORD and self.doKeywords: + tmpResult.append(self._formatKeywords(tText)) + + self.theResult = "".join(tmpResult) + tmpResult = [] + + self.fullMD.append(self.theResult) + + return + + def saveMarkdown(self, savePath): + """Save the data to a plain text file file. + """ + with open(savePath, mode="w", encoding="utf8") as outFile: + theText = "".join(self.fullMD) + outFile.write(theText) + + return + + def replaceTabs(self, nSpaces=8, spaceChar=" "): + """Replace tabs with spaces. + """ + fullMD = [] + eightSpace = spaceChar*nSpaces + for aPage in self.fullMD: + fullMD.append(aPage.replace("\t", eightSpace)) + + self.fullMD = fullMD + return + + ## + # Internal Functions + ## + + def _formatSynopsis(self, tText): + """Apply Markdown formatting to synopsis. + """ + if self.genMode == self.M_NW: + return "%% Synopsis: %s\n" % tText + else: + return "**Synopsis:** %s\n" % tText + + def _formatComments(self, tText): + """Apply Markdown formatting to comments. + """ + if self.genMode == self.M_NW: + return "%% %s\n" % tText + else: + return "**Comment:** %s\n" % tText + + def _formatKeywords(self, tText): + """Apply Markdown formatting to keywords. + """ + isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText) + if not isValid or not theBits: + return "" + + retText = "" + if theBits[0] in nwLabels.KEY_NAME: + if self.genMode == self.M_NW: + retText += "@%s: " % theBits[0] + else: + retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]] + + if len(theBits) > 1: + retText += ", ".join(theBits[1:]) + + return retText + +# END Class ToMarkdown From 9861f294b703603832455dd21aa0e126f90b8be1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 15:20:03 +0100 Subject: [PATCH 052/104] Connect markdown exporter to build tool --- nw/core/__init__.py | 2 + nw/gui/build.py | 102 +++++++++++++++++++++++++------------------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/nw/core/__init__.py b/nw/core/__init__.py index fc90379b..61046eec 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -6,6 +6,7 @@ from nw.core.project import NWProject from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple from nw.core.tohtml import ToHtml from nw.core.toodt import ToOdt +from nw.core.tomd import ToMarkdown from nw.core.tools import countWords, numberToRoman, numberToWord __all__ = [ @@ -20,4 +21,5 @@ __all__ = [ "NWSpellSimple", "ToHtml", "ToOdt", + "ToMarkdown", ] diff --git a/nw/gui/build.py b/nw/gui/build.py index 18bb4dc7..cfc28d86 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -46,7 +46,7 @@ from PyQt5.QtWidgets import ( from nw.common import fuzzyTime, makeFileNameSafe from nw.gui.custom import QSwitch -from nw.core import ToHtml, ToOdt +from nw.core import ToHtml, ToOdt, ToMarkdown from nw.constants import ( nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass ) @@ -60,10 +60,11 @@ class GuiBuildNovel(QDialog): FMT_PDF = 3 FMT_HTM = 4 FMT_MD = 5 - FMT_NWD = 6 - FMT_TXT = 7 - FMT_JSON_H = 8 - FMT_JSON_M = 9 + FMT_GH = 6 + FMT_NWD = 7 + FMT_TXT = 8 + FMT_JSON_H = 9 + FMT_JSON_M = 10 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -403,10 +404,13 @@ class GuiBuildNovel(QDialog): self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD)) self.saveMenu.addAction(self.saveNWD) - if self.mainConf.verQtValue >= 51400: - self.saveMD = QAction("Markdown (.md)", self) - self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) - self.saveMenu.addAction(self.saveMD) + self.saveMD = QAction("Standard Markdown (.md)", self) + self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD)) + self.saveMenu.addAction(self.saveMD) + + self.saveGH = QAction("GitHub Markdown (.md)", self) + self.saveGH.triggered.connect(lambda: self._saveDocument(self.FMT_GH)) + self.saveMenu.addAction(self.saveGH) self.saveTXT = QAction("Plain Text (.txt)", self) self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) @@ -619,6 +623,7 @@ class GuiBuildNovel(QDialog): isHtml = isinstance(bldObj, ToHtml) isOdt = isinstance(bldObj, ToOdt) + # isMd = isinstance(bldObj, ToMarkdown) bldObj.setTitleFormat(fmtTitle) bldObj.setChapterFormat(fmtChapter) @@ -739,7 +744,7 @@ class GuiBuildNovel(QDialog): return True - def _saveDocument(self, theFormat): + def _saveDocument(self, theFmt): """Save the document to various formats. """ replaceTabs = self.replaceTabs.isChecked() @@ -751,43 +756,46 @@ class GuiBuildNovel(QDialog): # Settings # ======== - if theFormat == self.FMT_ODT: + if theFmt == self.FMT_ODT: fileExt = "odt" textFmt = "Open Document" - elif theFormat == self.FMT_FODT: + elif theFmt == self.FMT_FODT: fileExt = "fodt" textFmt = "Flat Open Document" - elif theFormat == self.FMT_PDF: + elif theFmt == self.FMT_PDF: fileExt = "pdf" textFmt = "PDF" - elif theFormat == self.FMT_HTM: + elif theFmt == self.FMT_HTM: fileExt = "htm" textFmt = "Plain HTML" - elif theFormat == self.FMT_MD: - byteFmt.append("markdown") + elif theFmt == self.FMT_MD: fileExt = "md" - textFmt = "Markdown" + textFmt = "Standard Markdown" - elif theFormat == self.FMT_NWD: + elif theFmt == self.FMT_GH: + fileExt = "md" + textFmt = "GitHub Markdown" + + elif theFmt == self.FMT_NWD: fileExt = "nwd" - textFmt = "%s Markdown" % nw.__package__ + textFmt = "novelWriter Markdown" - elif theFormat == self.FMT_TXT: + elif theFmt == self.FMT_TXT: byteFmt.append("plaintext") fileExt = "txt" textFmt = "Plain Text" - elif theFormat == self.FMT_JSON_H: + elif theFmt == self.FMT_JSON_H: fileExt = "json" - textFmt = "JSON + %s HTML" % nw.__package__ + textFmt = "JSON + novelWriter HTML" - elif theFormat == self.FMT_JSON_M: + elif theFmt == self.FMT_JSON_M: fileExt = "json" - textFmt = "JSON + %s Markdown" % nw.__package__ + textFmt = "JSON + novelWriter Markdown" else: return False @@ -823,13 +831,13 @@ class GuiBuildNovel(QDialog): errMsg = "" wSuccess = False - if theFormat == self.FMT_MD or theFormat == self.FMT_TXT: + if theFmt == self.FMT_TXT: docWriter = QTextDocumentWriter() docWriter.setFileName(savePath) docWriter.setFormat(byteFmt) wSuccess = docWriter.write(self.docView.qDocument) - elif theFormat == self.FMT_HTM: + elif theFmt == self.FMT_HTM: makeHtml = ToHtml(self.theProject, self.theParent) self._doBuild(makeHtml) if replaceTabs: @@ -841,22 +849,26 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif theFormat == self.FMT_NWD: - makeNwd = ToHtml(self.theProject, self.theParent) - makeNwd.setKeepMarkdown(True) - self._doBuild(makeNwd, doConvert=False) + elif theFmt in (self.FMT_NWD, self.FMT_MD, self.FMT_GH): + makeMd = ToMarkdown(self.theProject, self.theParent) + if theFmt == self.FMT_NWD: + makeMd.setNovelWriterMarkdown() + elif theFmt == self.FMT_GH: + makeMd.setGitHubMarkdown + else: + makeMd.setStandardMarkdown() + + self._doBuild(makeMd) if replaceTabs: - makeNwd.replaceTabs(spaceChar=" ") + makeMd.replaceTabs(spaceChar=" ") try: - with open(savePath, mode="w", encoding="utf8") as outFile: - for nwdPage in makeNwd.theMarkdown: - outFile.write(nwdPage) + makeMd.saveMarkdown(savePath) wSuccess = True except Exception as e: errMsg = str(e) - elif theFormat == self.FMT_FODT: + elif theFmt == self.FMT_FODT: makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True) self._doBuild(makeOdt) try: @@ -865,7 +877,7 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif theFormat == self.FMT_ODT: + elif theFmt == self.FMT_ODT: makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False) self._doBuild(makeOdt) try: @@ -874,7 +886,7 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif theFormat == self.FMT_JSON_H or theFormat == self.FMT_JSON_M: + elif theFmt == self.FMT_JSON_H or theFmt == self.FMT_JSON_M: jsonData = { "meta" : { "workingTitle" : self.theProject.projName, @@ -884,7 +896,7 @@ class GuiBuildNovel(QDialog): } } - if theFormat == self.FMT_JSON_H: + if theFmt == self.FMT_JSON_H: makeHtml = ToHtml(self.theProject, self.theParent) self._doBuild(makeHtml) if replaceTabs: @@ -898,15 +910,15 @@ class GuiBuildNovel(QDialog): "html" : theBody, } - elif theFormat == self.FMT_JSON_M: - makeNwd = ToHtml(self.theProject, self.theParent) - makeNwd.setKeepMarkdown(True) - self._doBuild(makeNwd, doConvert=False) + elif theFmt == self.FMT_JSON_M: + makeMd = ToHtml(self.theProject, self.theParent) + makeMd.setKeepMarkdown(True) + self._doBuild(makeMd, doConvert=False) if replaceTabs: - makeNwd.replaceTabs(spaceChar=" ") + makeMd.replaceTabs(spaceChar=" ") theBody = [] - for nwdPage in makeNwd.theMarkdown: + for nwdPage in makeMd.theMarkdown: theBody.append(nwdPage.split("\n")) jsonData["text"] = { "nwd" : theBody, @@ -919,7 +931,7 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif theFormat == self.FMT_PDF: + elif theFmt == self.FMT_PDF: try: thePrinter = QPrinter() thePrinter.setOutputFormat(QPrinter.PdfFormat) From c677db70b4c252c0fcbdd7829f98a6ac29753a80 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 16:38:06 +0100 Subject: [PATCH 053/104] Fix a few issues with markdown export --- nw/core/tohtml.py | 1 + nw/core/tomd.py | 36 ++++++++++++++++++++---------------- nw/gui/build.py | 4 ++-- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 1a5b345c..3aa6f5fc 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -168,6 +168,7 @@ class ToHtml(Tokenizer): parStyle = None tmpResult = [] hasHardBreak = False + for tType, tLine, tText, tFormat, tStyle in self.theTokens: # Styles diff --git a/nw/core/tomd.py b/nw/core/tomd.py index 35bcdf47..74c42a19 100644 --- a/nw/core/tomd.py +++ b/nw/core/tomd.py @@ -58,7 +58,7 @@ class ToMarkdown(Tokenizer): return def setNovelWriterMarkdown(self): - self.genMode = self.M_MD + self.genMode = self.M_NW return ## @@ -100,29 +100,28 @@ class ToMarkdown(Tokenizer): if tType == self.T_EMPTY: if len(thisPar) > 0: tTemp = "".join(thisPar) - tmpResult.append("%s\n" % tTemp) - + tmpResult.append("%s\n\n" % tTemp.rstrip(" ")) thisPar = [] elif tType == self.T_TITLE: tHead = tText.replace(r"\\", "\n") - tmpResult.append("# %s\n" % tHead) + tmpResult.append("# %s\n\n" % tHead) elif tType == self.T_HEAD1: tHead = tText.replace(r"\\", "\n") - tmpResult.append("# %s\n" % tHead) + tmpResult.append("# %s\n\n" % tHead) elif tType == self.T_HEAD2: tHead = tText.replace(r"\\", "\n") - tmpResult.append("## %s\n" % tHead) + tmpResult.append("## %s\n\n" % tHead) elif tType == self.T_HEAD3: tHead = tText.replace(r"\\", "\n") - tmpResult.append("### %s\n" % tHead) + tmpResult.append("### %s\n\n" % tHead) elif tType == self.T_HEAD4: tHead = tText.replace(r"\\", "\n") - tmpResult.append("#### %s\n" % tHead) + tmpResult.append("#### %s\n\n" % tHead) elif tType == self.T_SEP: tmpResult.append("%s\n\n" % tText) @@ -135,7 +134,7 @@ class ToMarkdown(Tokenizer): for xPos, xLen, xFmt in reversed(tFormat): tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:] if tText.endswith(" "): - thisPar.append(tTemp.rstrip() + " ") + thisPar.append(tTemp.rstrip() + " \n") else: thisPar.append(tTemp.rstrip() + " ") @@ -146,7 +145,7 @@ class ToMarkdown(Tokenizer): tmpResult.append(self._formatComments(tText)) elif tType == self.T_KEYWORD and self.doKeywords: - tmpResult.append(self._formatKeywords(tText)) + tmpResult.append(self._formatKeywords(tText, tStyle)) self.theResult = "".join(tmpResult) tmpResult = [] @@ -183,19 +182,19 @@ class ToMarkdown(Tokenizer): """Apply Markdown formatting to synopsis. """ if self.genMode == self.M_NW: - return "%% Synopsis: %s\n" % tText + return "%% Synopsis: %s\n\n" % tText else: - return "**Synopsis:** %s\n" % tText + return "**Synopsis:** %s\n\n" % tText def _formatComments(self, tText): """Apply Markdown formatting to comments. """ if self.genMode == self.M_NW: - return "%% %s\n" % tText + return "%% %s\n\n" % tText else: - return "**Comment:** %s\n" % tText + return "**Comment:** %s\n\n" % tText - def _formatKeywords(self, tText): + def _formatKeywords(self, tText, tStyle): """Apply Markdown formatting to keywords. """ isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText) @@ -205,13 +204,18 @@ class ToMarkdown(Tokenizer): retText = "" if theBits[0] in nwLabels.KEY_NAME: if self.genMode == self.M_NW: - retText += "@%s: " % theBits[0] + retText += "%s: " % theBits[0] else: retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]] if len(theBits) > 1: retText += ", ".join(theBits[1:]) + if tStyle & self.A_Z_BTMMRG and self.genMode != self.M_NW: + retText += " \n" + else: + retText += "\n\n" + return retText # END Class ToMarkdown diff --git a/nw/gui/build.py b/nw/gui/build.py index cfc28d86..f60d333b 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -854,13 +854,13 @@ class GuiBuildNovel(QDialog): if theFmt == self.FMT_NWD: makeMd.setNovelWriterMarkdown() elif theFmt == self.FMT_GH: - makeMd.setGitHubMarkdown + makeMd.setGitHubMarkdown() else: makeMd.setStandardMarkdown() self._doBuild(makeMd) if replaceTabs: - makeMd.replaceTabs(spaceChar=" ") + makeMd.replaceTabs(nSpaces=4, spaceChar=" ") try: makeMd.saveMarkdown(savePath) From b59b3a3a7483da82334d746173c09315f3e5df57 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:19:30 +0100 Subject: [PATCH 054/104] Use the old converter for nwd file, not the markdown class --- nw/core/tokenizer.py | 8 ++++++ nw/core/tomd.py | 34 ++++---------------------- nw/gui/build.py | 20 +++++++++++---- tests/test_core/test_core_tokenizer.py | 13 ++++++---- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index a1ffe11f..96b4be2d 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -628,6 +628,14 @@ class Tokenizer(): return True + def saveRawMarkdown(self, savePath): + """Save the data to a plain text file. + """ + with open(savePath, mode="w", encoding="utf8") as outFile: + for nwdPage in self.theMarkdown: + outFile.write(nwdPage) + return + ## # Internal Functions ## diff --git a/nw/core/tomd.py b/nw/core/tomd.py index 74c42a19..fe6da8fa 100644 --- a/nw/core/tomd.py +++ b/nw/core/tomd.py @@ -35,7 +35,6 @@ class ToMarkdown(Tokenizer): M_STD = 0 # Standard Markdown M_GH = 1 # GitHub Markdown - M_NW = 2 # novelWriter Markdown def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) @@ -57,10 +56,6 @@ class ToMarkdown(Tokenizer): self.genMode = self.M_GH return - def setNovelWriterMarkdown(self): - self.genMode = self.M_NW - return - ## # Class Methods ## @@ -139,10 +134,10 @@ class ToMarkdown(Tokenizer): thisPar.append(tTemp.rstrip() + " ") elif tType == self.T_SYNOPSIS and self.doSynopsis: - tmpResult.append(self._formatSynopsis(tText)) + tmpResult.append("**Synopsis:** %s\n\n" % tText) elif tType == self.T_COMMENT and self.doComments: - tmpResult.append(self._formatComments(tText)) + tmpResult.append("**Comment:** %s\n\n" % tText) elif tType == self.T_KEYWORD and self.doKeywords: tmpResult.append(self._formatKeywords(tText, tStyle)) @@ -155,7 +150,7 @@ class ToMarkdown(Tokenizer): return def saveMarkdown(self, savePath): - """Save the data to a plain text file file. + """Save the data to a plain text file. """ with open(savePath, mode="w", encoding="utf8") as outFile: theText = "".join(self.fullMD) @@ -178,22 +173,6 @@ class ToMarkdown(Tokenizer): # Internal Functions ## - def _formatSynopsis(self, tText): - """Apply Markdown formatting to synopsis. - """ - if self.genMode == self.M_NW: - return "%% Synopsis: %s\n\n" % tText - else: - return "**Synopsis:** %s\n\n" % tText - - def _formatComments(self, tText): - """Apply Markdown formatting to comments. - """ - if self.genMode == self.M_NW: - return "%% %s\n\n" % tText - else: - return "**Comment:** %s\n\n" % tText - def _formatKeywords(self, tText, tStyle): """Apply Markdown formatting to keywords. """ @@ -203,15 +182,12 @@ class ToMarkdown(Tokenizer): retText = "" if theBits[0] in nwLabels.KEY_NAME: - if self.genMode == self.M_NW: - retText += "%s: " % theBits[0] - else: - retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]] + retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]] if len(theBits) > 1: retText += ", ".join(theBits[1:]) - if tStyle & self.A_Z_BTMMRG and self.genMode != self.M_NW: + if tStyle & self.A_Z_BTMMRG: retText += " \n" else: retText += "\n\n" diff --git a/nw/gui/build.py b/nw/gui/build.py index f60d333b..58f33766 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -623,7 +623,6 @@ class GuiBuildNovel(QDialog): isHtml = isinstance(bldObj, ToHtml) isOdt = isinstance(bldObj, ToOdt) - # isMd = isinstance(bldObj, ToMarkdown) bldObj.setTitleFormat(fmtTitle) bldObj.setChapterFormat(fmtChapter) @@ -849,11 +848,22 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif theFmt in (self.FMT_NWD, self.FMT_MD, self.FMT_GH): + elif theFmt == self.FMT_NWD: + makeNwd = ToMarkdown(self.theProject, self.theParent) + makeNwd.setKeepMarkdown(True) + self._doBuild(makeNwd, doConvert=False) + if replaceTabs: + makeNwd.replaceTabs(spaceChar=" ") + + try: + makeNwd.saveRawMarkdown(savePath) + wSuccess = True + except Exception as e: + errMsg = str(e) + + elif theFmt in (self.FMT_MD, self.FMT_GH): makeMd = ToMarkdown(self.theProject, self.theParent) - if theFmt == self.FMT_NWD: - makeMd.setNovelWriterMarkdown() - elif theFmt == self.FMT_GH: + if theFmt == self.FMT_GH: makeMd.setGitHubMarkdown() else: makeMd.setStandardMarkdown() diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 6cee910f..f624fec3 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -20,8 +20,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os import pytest +from tools import readFile + from nw.core import NWProject, NWDoc from nw.core.tokenizer import Tokenizer @@ -182,6 +185,11 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI): theToken.doPostProcessing() assert theToken.theResult == "This is text with escapes: ** ~~ __" + # Save File + savePath = os.path.join(nwMinimal, "dump.nwd") + theToken.saveRawMarkdown(savePath) + assert readFile(savePath) == "# Notes: Plot\n\n" + # END Test testCoreToken_TextOps @pytest.mark.core @@ -397,11 +405,6 @@ def testCoreToken_Tokenize(dummyGUI): "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" ) - # Check the markdown function as well - assert theToken.theMarkdown[-1] == ( - "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" - ) - # END Test testCoreToken_Tokenize @pytest.mark.core From f4988c5d86d62c374b45b76ac14434045ccc6d95 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:40:39 +0100 Subject: [PATCH 055/104] Clean up build tool and extend export tests --- nw/gui/build.py | 112 +++++------ .../guiBuild_Tool_Step1G_Lorem_Ipsum.md | 88 +++++++++ .../guiBuild_Tool_Step1_Lorem_Ipsum.md | 88 +++++++++ .../guiBuild_Tool_Step2_Lorem_Ipsum.md | 182 ++++++++++++++++++ .../guiBuild_Tool_Step3_Lorem_Ipsum.md | 182 ++++++++++++++++++ tests/test_gui/test_gui_build.py | 34 +++- 6 files changed, 613 insertions(+), 73 deletions(-) create mode 100644 tests/reference/guiBuild_Tool_Step1G_Lorem_Ipsum.md create mode 100644 tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.md create mode 100644 tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.md create mode 100644 tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.md diff --git a/nw/gui/build.py b/nw/gui/build.py index 58f33766..cfe07eb7 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -35,7 +35,7 @@ from datetime import datetime from PyQt5.QtCore import Qt, QByteArray, QTimer from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtGui import ( - QPalette, QColor, QTextDocumentWriter, QFont, QCursor, QFontInfo + QPalette, QColor, QFont, QCursor, QFontInfo ) from PyQt5.QtWidgets import ( qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, @@ -62,9 +62,8 @@ class GuiBuildNovel(QDialog): FMT_MD = 5 FMT_GH = 6 FMT_NWD = 7 - FMT_TXT = 8 - FMT_JSON_H = 9 - FMT_JSON_M = 10 + FMT_JSON_H = 8 + FMT_JSON_M = 9 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -377,11 +376,24 @@ class GuiBuildNovel(QDialog): self.buttonBox = QHBoxLayout() - self.btnPrint = QPushButton("Print") - self.btnPrint.clicked.connect(self._printDocument) + # Printing + + self.printMenu = QMenu(self) + self.btnPrint = QPushButton("Print") + self.btnPrint.setMenu(self.printMenu) + + self.printSend = QAction("Send to Printer", self) + self.printSend.triggered.connect(self._printDocument) + self.printMenu.addAction(self.printSend) + + self.printFile = QAction("Print to PDF", self) + self.printFile.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) + self.printMenu.addAction(self.printFile) + + # Saving to File - self.btnSave = QPushButton("Save As") self.saveMenu = QMenu(self) + self.btnSave = QPushButton("Save As") self.btnSave.setMenu(self.saveMenu) self.saveODT = QAction("Open Document (.odt)", self) @@ -392,10 +404,6 @@ class GuiBuildNovel(QDialog): self.saveFODT.triggered.connect(lambda: self._saveDocument(self.FMT_FODT)) self.saveMenu.addAction(self.saveFODT) - self.savePDF = QAction("Portable Document Format (.pdf)", self) - self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) - self.saveMenu.addAction(self.savePDF) - self.saveHTM = QAction("novelWriter HTML (.htm)", self) self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM)) self.saveMenu.addAction(self.saveHTM) @@ -412,10 +420,6 @@ class GuiBuildNovel(QDialog): self.saveGH.triggered.connect(lambda: self._saveDocument(self.FMT_GH)) self.saveMenu.addAction(self.saveGH) - self.saveTXT = QAction("Plain Text (.txt)", self) - self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) - self.saveMenu.addAction(self.saveTXT) - self.saveJsonH = QAction("JSON + novelWriter HTML (.json)", self) self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H)) self.saveMenu.addAction(self.saveJsonH) @@ -530,7 +534,6 @@ class GuiBuildNovel(QDialog): self.docView.setText( "Failed to generate preview. The result is too big." ) - self._enableQtSave(False) else: self.htmlText = [] @@ -583,12 +586,10 @@ class GuiBuildNovel(QDialog): if self.htmlSize < nwConst.MAX_BUILDSIZE: self.docView.setContent(self.htmlText, self.buildTime) - self._enableQtSave(True) else: self.docView.setText( "Failed to generate preview. The result is too big." ) - self._enableQtSave(False) self._saveCache() @@ -676,7 +677,7 @@ class GuiBuildNovel(QDialog): bldObj.doPostProcessing() except Exception: - logger.error("Failed to generate html of document '%s'" % tItem.itemHandle) + logger.error("Failed to build document '%s'" % tItem.itemHandle) nw.logException() if isPreview: self.docView.setText(( @@ -748,7 +749,6 @@ class GuiBuildNovel(QDialog): """ replaceTabs = self.replaceTabs.isChecked() - byteFmt = QByteArray() fileExt = "" textFmt = "" @@ -763,14 +763,14 @@ class GuiBuildNovel(QDialog): fileExt = "fodt" textFmt = "Flat Open Document" - elif theFmt == self.FMT_PDF: - fileExt = "pdf" - textFmt = "PDF" - elif theFmt == self.FMT_HTM: fileExt = "htm" textFmt = "Plain HTML" + elif theFmt == self.FMT_NWD: + fileExt = "nwd" + textFmt = "novelWriter Markdown" + elif theFmt == self.FMT_MD: fileExt = "md" textFmt = "Standard Markdown" @@ -779,15 +779,6 @@ class GuiBuildNovel(QDialog): fileExt = "md" textFmt = "GitHub Markdown" - elif theFmt == self.FMT_NWD: - fileExt = "nwd" - textFmt = "novelWriter Markdown" - - elif theFmt == self.FMT_TXT: - byteFmt.append("plaintext") - fileExt = "txt" - textFmt = "Plain Text" - elif theFmt == self.FMT_JSON_H: fileExt = "json" textFmt = "JSON + novelWriter HTML" @@ -796,6 +787,10 @@ class GuiBuildNovel(QDialog): fileExt = "json" textFmt = "JSON + novelWriter Markdown" + elif theFmt == self.FMT_PDF: + fileExt = "pdf" + textFmt = "PDF" + else: return False @@ -830,11 +825,23 @@ class GuiBuildNovel(QDialog): errMsg = "" wSuccess = False - if theFmt == self.FMT_TXT: - docWriter = QTextDocumentWriter() - docWriter.setFileName(savePath) - docWriter.setFormat(byteFmt) - wSuccess = docWriter.write(self.docView.qDocument) + if theFmt == self.FMT_ODT: + makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False) + self._doBuild(makeOdt) + try: + makeOdt.saveOpenDocText(savePath) + wSuccess = True + except Exception as e: + errMsg = str(e) + + elif theFmt == self.FMT_FODT: + makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True) + self._doBuild(makeOdt) + try: + makeOdt.saveFlatXML(savePath) + wSuccess = True + except Exception as e: + errMsg = str(e) elif theFmt == self.FMT_HTM: makeHtml = ToHtml(self.theProject, self.theParent) @@ -878,24 +885,6 @@ class GuiBuildNovel(QDialog): except Exception as e: errMsg = str(e) - elif theFmt == self.FMT_FODT: - makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True) - self._doBuild(makeOdt) - try: - makeOdt.saveFlatXML(savePath) - wSuccess = True - except Exception as e: - errMsg = str(e) - - elif theFmt == self.FMT_ODT: - makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False) - self._doBuild(makeOdt) - try: - makeOdt.saveOpenDocText(savePath) - wSuccess = True - except Exception as e: - errMsg = str(e) - elif theFmt == self.FMT_JSON_H or theFmt == self.FMT_JSON_M: jsonData = { "meta" : { @@ -1077,17 +1066,6 @@ class GuiBuildNovel(QDialog): # Internal Functions ## - def _enableQtSave(self, theState): - """Set the enabled status of Save menu entries that depend on - the QTextDocument. - """ - self.saveODT.setEnabled(theState) - self.savePDF.setEnabled(theState) - self.saveTXT.setEnabled(theState) - if self.mainConf.verQtValue >= 51400: - self.saveMD.setEnabled(theState) - return - def _saveSettings(self): """Save the various user settings. """ diff --git a/tests/reference/guiBuild_Tool_Step1G_Lorem_Ipsum.md b/tests/reference/guiBuild_Tool_Step1G_Lorem_Ipsum.md new file mode 100644 index 00000000..8a24d29f --- /dev/null +++ b/tests/reference/guiBuild_Tool_Step1G_Lorem_Ipsum.md @@ -0,0 +1,88 @@ +# Lorem Ipsum + +**By lipsum.com** + +“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” + +“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + +## Prologue + +_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + +# Act One + +“Fusce maximus felis libero” + +## Chapter 1: Chapter One + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar. + +* * * + +Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada. + +Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex. + +Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. + +Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend. + +* * * + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien. + +Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis. + +Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi. + +Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. + +Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor. + +Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus. + +## Chapter 2: Chapter Two + +Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at. + +* * * + +Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis. + +Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis. + +Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus. + +Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac. + +* * * + +Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien. + +Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum. + +Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit. + +Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien. + +Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui. + +Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero. + +* * * + +Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet. + +In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere. + +Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis. + +Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue. + +Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo. + diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.md b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.md new file mode 100644 index 00000000..8a24d29f --- /dev/null +++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.md @@ -0,0 +1,88 @@ +# Lorem Ipsum + +**By lipsum.com** + +“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” + +“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + +## Prologue + +_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + +# Act One + +“Fusce maximus felis libero” + +## Chapter 1: Chapter One + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar. + +* * * + +Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada. + +Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex. + +Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. + +Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend. + +* * * + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien. + +Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis. + +Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi. + +Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. + +Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor. + +Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus. + +## Chapter 2: Chapter Two + +Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at. + +* * * + +Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis. + +Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis. + +Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus. + +Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac. + +* * * + +Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien. + +Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum. + +Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit. + +Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien. + +Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui. + +Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero. + +* * * + +Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet. + +In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere. + +Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis. + +Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue. + +Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo. + diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.md b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.md new file mode 100644 index 00000000..69f43dcf --- /dev/null +++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.md @@ -0,0 +1,182 @@ +# Lorem Ipsum + +**By lipsum.com** + +“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” + +“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” + +**Comment:** Exctracted from the lipsum.com website. + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + +## Prologue + +**Synopsis:** Explanation from the lipsum.com website. + +_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + +# Act One + +“Fusce maximus felis libero” + +## Chapter One: Chapter One + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar. + +### Scene 1.1: Scene One + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. + +Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada. + +Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex. + +#### 1.1.1: Scene One, Section Two + +Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. + +Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend. + +### Scene 1.2: Scene Two + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien. + +Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis. + +Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi. + +#### 1.2.1: Scene Two, Section Two + +Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. + +Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor. + +Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus. + +## Chapter Two: Why do we use it? + +**Comment:** Exctracted from the lipsum.com website. + + It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. + + The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. + + Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like). + +## Chapter Three: Chapter Two + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. + +Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at. + +### Scene 3.1: Scene Three + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. + +Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis. + +Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis. + +Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus. + +Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac. + +### Scene 3.2: Scene Four + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. + +Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien. + +Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum. + +Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit. + +Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien. + +Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui. + +Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero. + +### Scene 3.3: Scene Five + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet. + +In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere. + +Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis. + +Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue. + +Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo. + +# Notes: Characters + +# Nobody Owens + +**Tag:** Bod + +Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus. + +Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat. + +Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque. + +# Notes: Plot + +# Main Plot + +**Tag:** Main + +Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet. + +Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur. + +# Notes: World + +# Ancient Europe + +**Tag:** Europe + +Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula. + +Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque. + +Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim. + diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.md b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.md new file mode 100644 index 00000000..aea72b0a --- /dev/null +++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.md @@ -0,0 +1,182 @@ +# Lorem Ipsum + +**By lipsum.com** + +“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” + +“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” + +**Comment:** Exctracted from the lipsum.com website. + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + +## Prologue + +**Synopsis:** Explanation from the lipsum.com website. + +_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + +# Act One + +“Fusce maximus felis libero” + +## Chapter One: Chapter One + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar. + +### Scene 1.1: Scene One + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. + +Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada. + +Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex. + +#### 1.1.1: Scene One, Section Two + +Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. + +Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend. + +### Scene 1.2: Scene Two + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. + +Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien. + +Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis. + +Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi. + +#### 1.2.1: Scene Two, Section Two + +Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. + +Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor. + +Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus. + +## Chapter Two: Why do we use it? + +**Comment:** Exctracted from the lipsum.com website. + + It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. + + The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. + + Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like). + +## Chapter Three: Chapter Two + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. + +Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at. + +### Scene 3.1: Scene Three + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. + +Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis. + +Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis. + +Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus. + +Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac. + +### Scene 3.2: Scene Four + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. + +Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien. + +Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum. + +Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit. + +Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien. + +Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui. + +Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero. + +### Scene 3.3: Scene Five + +**Point of View:** Bod +**Plot:** Main +**Locations:** Europe + +**Synopsis:** Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet. + +In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere. + +Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis. + +Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue. + +Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo. + +# Notes: Characters + +# Nobody Owens + +**Tag:** Bod + +Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus. + +Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat. + +Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque. + +# Notes: Plot + +# Main Plot + +**Tag:** Main + +Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet. + +Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur. + +# Notes: World + +# Ancient Europe + +**Tag:** Europe + +Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula. + +Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque. + +Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim. + diff --git a/tests/test_gui/test_gui_build.py b/tests/test_gui/test_gui_build.py index 38813f00..a6927476 100644 --- a/tests/test_gui/test_gui_build.py +++ b/tests/test_gui/test_gui_build.py @@ -80,6 +80,20 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) + assert nwBuild._saveDocument(nwBuild.FMT_MD) + projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") + testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md") + compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md") + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile) + + assert nwBuild._saveDocument(nwBuild.FMT_GH) + projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") + testFile = os.path.join(outDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md") + compFile = os.path.join(refDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md") + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile) + # Change Title Formats and Flip Switches nwBuild.fmtChapter.setText(r"Chapter %chw%: %title%") qtbot.wait(stepDelay) @@ -119,6 +133,13 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) + assert nwBuild._saveDocument(nwBuild.FMT_MD) + projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") + testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md") + compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md") + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile) + # Replace Tabs with Spaces qtbot.mouseClick(nwBuild.replaceTabs, Qt.LeftButton) qtbot.wait(stepDelay) @@ -140,6 +161,13 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) + assert nwBuild._saveDocument(nwBuild.FMT_MD) + projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") + testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md") + compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md") + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile) + # Putline Mode nwBuild.fmtChapter.setText(r"Chapter %chw%: %title%") qtbot.wait(stepDelay) @@ -199,12 +227,6 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): assert nwBuild._saveDocument(nwBuild.FMT_PDF) assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf")) - assert nwBuild._saveDocument(nwBuild.FMT_MD) - assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.md")) - - assert nwBuild._saveDocument(nwBuild.FMT_TXT) - assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.txt")) - # Close the build tool htmlText = nwBuild.htmlText htmlStyle = nwBuild.htmlStyle From 2fd940bcde1b3091d638c0c0d2dda22df5412979 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:48:59 +0100 Subject: [PATCH 056/104] Update documentation --- docs/source/usage_export.rst | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/source/usage_export.rst b/docs/source/usage_export.rst index 76ad3237..b9968e6b 100644 --- a/docs/source/usage_export.rst +++ b/docs/source/usage_export.rst @@ -102,6 +102,16 @@ document has a checkmark after the status icon in the :guilabel:`Flags` column. override these settings. +.. _a_export_print: + +Printing +======== + +The print button allows you to print the content in the preview window. You can either print to one +of your system's printers, or print directly to file. You can also print to file from the regular +print dialog. The direct to file option is just a shortcut. + + .. _a_export_formats: Export Formats @@ -110,21 +120,13 @@ Export Formats Currently, six formats are supported for exporting. OpenDocument Format - This produces an open document ``.odt`` file. The document produced has very little formatting, - and may require further editing afterwards. For a better formatted office document, you may get - a better result by exporting to HTML and then importing that HTML document into your office word - processor. They are generally good at importing HTML documents. - -PDF Format - The PDF export is just a shortcut for print-to-file. For a better PDF result, you may instead - want to export to HTML and use a word processor to convert the HTML document to PDF. + The Build tool can produce either an ``.odt`` file, of a ``.fodt`` file. The latter is just a + flat version of the document format as a single XML file. novelWriter HTML The HTML export format writes a single ``.htm`` file with minimal style formatting. The exported HTML document is suitable for further processing by document conversion tools like Pandoc, for - importing in word processors, or for printing from browser. It is generally the best formatted - export option and supports all features of novelWriter since it is entirely generated by the - application and doesn't depend on Qt library features. + importing in word processors, or for printing from browser. novelWriter Markdown This is simply a concatenation of the project documents selected by the filters. The documents @@ -132,12 +134,10 @@ novelWriter Markdown included if they are selected. This is a useful format for exporting the project for later import back into novelWriter. -Standard Markdown - If you have Qt 5.14 or higher, the option to export to plain markdown is available. This feature - uses Qt's own markdown export feature. - -Plain Text - The plain text export format writes a simple ``.txt`` file without any formatting at all. +Standard/GitHub Markdown + The Markdown export format comes in both Standard and GitHub flavour. The *only* difference in + terms of novelWriter functionality is the support of strikethrough text, which is not supported + by the Standard flavour, but *is* supported by the GitHub flavour. .. _a_export_options: From baf9cab66c0db5567f50ca8a51f226bdd7c6289d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 17:55:29 +0100 Subject: [PATCH 057/104] Fix a few other docs sections about export --- docs/source/int_introduction.rst | 10 ++++------ docs/source/int_started.rst | 7 +++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/source/int_introduction.rst b/docs/source/int_introduction.rst index 4e827724..8bc8b3ad 100644 --- a/docs/source/int_introduction.rst +++ b/docs/source/int_introduction.rst @@ -93,13 +93,11 @@ Project Export ============== The project can at any time be exported to a range of different formats through the -:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to plain text file, -HTML document, novelWriter flavoured markdown, standard markdown (requires Qt 5.14), and to a basic -Open Document format. +:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to Open Document, +HTML5, and various flavours of Markdown. -In addition, printing and printing to PDF is also possible. The best supported export format is -HTML, which can be imported or converted by a number of other tools like Pandoc, or simply imported -into Libre Office Writer and similar word processors. +The HTML5 export format is suitable for convertion by a number of other tools like Pandoc, or for +importing into word processors. In addition, printing and printing to PDF is also possible. It is also possible to export the content of the project to a JSON file. This is useful if you want to write your own processing script in for instance Python as the entire novel can be read into a diff --git a/docs/source/int_started.rst b/docs/source/int_started.rst index 4feb9c8c..6c2b596c 100644 --- a/docs/source/int_started.rst +++ b/docs/source/int_started.rst @@ -44,10 +44,9 @@ The following Python packages are needed to run novelWriter: * ``pyenchant`` – needed for efficient spell checking (optional). PyQt/Qt should be at least 5.2.1, but ideally 5.10 or higher for nearly all features to work. -Exporting to standard Markdown, for instance, requires PyQt/Qt 5.14. Searching using regular -expressions requires 5.3, and for full Unicode support, 5.13. There is no known minimum version -requirement for package ``lxml``, but the code was originally written with 4.2, which is therefore -set as the minimum. It may work on lower versions. You have to test it. +Searching using regular expressions requires 5.3, and for full Unicode support, 5.13. There is no +known minimum version requirement for package ``lxml``, but the code was originally written with +4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it. Optionally, a package can be installed to interface with the Enchant spell checking libaries, but this isn't strictly required. If no external spell checking library is available, novelWriter falls From e645c19761fd3e17e7a2bae911b0ccee3339efac Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:01:03 +0100 Subject: [PATCH 058/104] Fix flake8 error --- nw/gui/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index cfe07eb7..dae234c0 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -32,7 +32,7 @@ import os from time import time from datetime import datetime -from PyQt5.QtCore import Qt, QByteArray, QTimer +from PyQt5.QtCore import Qt, QTimer from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtGui import ( QPalette, QColor, QFont, QCursor, QFontInfo From 74a9ddbe4716d780d3c28887cc63138e750294ef Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:29:59 +0100 Subject: [PATCH 059/104] Fix wrong version in ToOdt class header --- nw/core/toodt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 936c7faf..78cab3d3 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -5,7 +5,7 @@ novelWriter – ODT Text Converter Extends the Tokenizer class to generate ODT and FODT files File History: -Created: 2021-01-26 [1.1rc1] +Created: 2021-01-26 [1.2a0] This file is a part of novelWriter Copyright 2018–2021, Veronica Berglyd Olsen From 5e40bd5deb3b449d6e87f34889b3b378ca181c56 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:31:37 +0100 Subject: [PATCH 060/104] Change text on build tool print button --- nw/gui/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index dae234c0..f3d0cad5 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -382,7 +382,7 @@ class GuiBuildNovel(QDialog): self.btnPrint = QPushButton("Print") self.btnPrint.setMenu(self.printMenu) - self.printSend = QAction("Send to Printer", self) + self.printSend = QAction("Print Preview", self) self.printSend.triggered.connect(self._printDocument) self.printMenu.addAction(self.printSend) From 3db99879668e3a1d86afe9b3951985b74f1322be Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 23:22:23 +0100 Subject: [PATCH 061/104] Add a default header to Open Document files --- nw/core/toodt.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++- nw/gui/build.py | 5 +-- 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 78cab3d3..b45e5c04 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -80,9 +80,12 @@ class ToOdt(Tokenizer): self._xMeta = None # Office meta root self._xStyl = None # Office styles root self._xAuto = None # Office auto-styles root + self._xMast = None # Office master-styles root self._xBody = None # Office body root self._xText = None # Office text root + self._xAut2 = None # Page layout auto-styles for ODT file + self._mainPara = {} # User-accessible paragraph styles self._autoPara = {} # Auto-generated paragraph styles self._autoText = {} # Auto-generated text styles @@ -92,6 +95,8 @@ class ToOdt(Tokenizer): self.textSize = 12 self.textFixed = False self.colourHead = False + self.addHeader = True + self.headerText = "" # Internal self._fontFamily = "'Liberation Sans'" @@ -212,6 +217,14 @@ class ToOdt(Tokenizer): self._lineHeight = f"{round(100 * self.lineHeight):d}%" self._textAlign = "justify" if self.doJustify else "left" + # Document Header + # =============== + + if self.headerText == "": + theTitle = self.theProject.bookTitle + theAuth = self.theProject.getAuthors() + self.headerText = f"{theTitle} / {theAuth} /" + # Create Roots # ============ @@ -236,6 +249,7 @@ class ToOdt(Tokenizer): self._xFont = etree.SubElement(self._dFlat, _mkTag("office", "font-face-decls")) self._xStyl = etree.SubElement(self._dFlat, _mkTag("office", "styles")) self._xAuto = etree.SubElement(self._dFlat, _mkTag("office", "automatic-styles")) + self._xMast = etree.SubElement(self._dFlat, _mkTag("office", "master-styles")) self._xBody = etree.SubElement(self._dFlat, _mkTag("office", "body")) etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr) @@ -249,17 +263,22 @@ class ToOdt(Tokenizer): tMeta = _mkTag("office", "document-meta") tStyl = _mkTag("office", "document-styles") + # content.xml self._dCont = etree.Element(tCont, attrib=tAttr, nsmap=XML_NS) self._xFnt1 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls")) self._xAuto = etree.SubElement(self._dCont, _mkTag("office", "automatic-styles")) self._xBody = etree.SubElement(self._dCont, _mkTag("office", "body")) + # meta.xml self._dMeta = etree.Element(tMeta, attrib=tAttr, nsmap=XML_NS) self._xMeta = etree.SubElement(self._dMeta, _mkTag("office", "meta")) + # styles.xml self._dStyl = etree.Element(tStyl, attrib=tAttr, nsmap=XML_NS) - self._xFnt2 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls")) + self._xFnt2 = etree.SubElement(self._dStyl, _mkTag("office", "font-face-decls")) self._xStyl = etree.SubElement(self._dStyl, _mkTag("office", "styles")) + self._xAut2 = etree.SubElement(self._dStyl, _mkTag("office", "automatic-styles")) + self._xMast = etree.SubElement(self._dStyl, _mkTag("office", "master-styles")) etree.SubElement(self._xFnt1, _mkTag("style", "font-face"), attrib=fAttr) etree.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr) @@ -276,8 +295,10 @@ class ToOdt(Tokenizer): xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator")) xMeta.text = f"novelWriter/{nw.__version__}" + self._pageStyles() self._defaultStyles() self._useableStyles() + self._writeHeader() return @@ -659,6 +680,38 @@ class ToOdt(Tokenizer): # Style Elements ## + def _pageStyles(self): + """Set the default page style. + """ + # If we're in flat layout, the page style goes to the main auto-styles + # In archived file, we make a duplicate auto-styles in the self._xPage + # variable which is later added to the styles.xml to go with the + # master-page definition + theAttr = {} + theAttr[_mkTag("style", "name")] = "PM1" + if self._isFlat: + xPage = etree.SubElement(self._xAuto, _mkTag("style", "page-layout"), attrib=theAttr) + else: + xPage = etree.SubElement(self._xAut2, _mkTag("style", "page-layout"), attrib=theAttr) + + theAttr = {} + theAttr[_mkTag("fo", "margin-top")] = "2.000cm" + theAttr[_mkTag("fo", "margin-bottom")] = "2.000cm" + theAttr[_mkTag("fo", "margin-left")] = "2.000cm" + theAttr[_mkTag("fo", "margin-right")] = "2.000cm" + etree.SubElement(xPage, _mkTag("style", "page-layout-properties"), attrib=theAttr) + + xHead = etree.SubElement(xPage, _mkTag("style", "header-style")) + + theAttr = {} + theAttr[_mkTag("fo", "min-height")] = "0.600cm" + theAttr[_mkTag("fo", "margin-left")] = "0.000cm" + theAttr[_mkTag("fo", "margin-right")] = "0.000cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.499cm" + etree.SubElement(xHead, _mkTag("style", "header-footer-properties"), attrib=theAttr) + + return + def _defaultStyles(self): """Set the default styles. """ @@ -721,6 +774,19 @@ class ToOdt(Tokenizer): theAttr[_mkTag("fo", "font-size")] = self._fSizeHead etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr) + # Add Header and Footer Styles + # ============================ + if not self.addHeader: + return + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Header_and_Footer" + theAttr[_mkTag("style", "display-name")] = "Header and Footer" + theAttr[_mkTag("style", "family")] = "paragraph" + theAttr[_mkTag("style", "parent-style-name")] = "Standard" + theAttr[_mkTag("style", "class")] = "extra" + etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr) + return def _useableStyles(self): @@ -866,6 +932,50 @@ class ToOdt(Tokenizer): self._mainPara["Heading_4"] = oStyle + # Add Header Style + # ================ + if not self.addHeader: + return + + oStyle = ODTParagraphStyle() + oStyle.setDisplayName("Header") + oStyle.setParentStyleName("Header_and_Footer") + oStyle.setTextAlign("right") + oStyle.packXML(self._xStyl, "Header") + + self._mainPara["Header"] = oStyle + + return + + def _writeHeader(self): + """Write the header elements. + """ + if not self.addHeader: + return + + theAttr = {} + theAttr[_mkTag("style", "name")] = "Standard" + theAttr[_mkTag("style", "page-layout-name")] = "PM1" + xPage = etree.SubElement(self._xMast, _mkTag("style", "master-page"), attrib=theAttr) + + # Standard Page Header + xHead = etree.SubElement(xPage, _mkTag("style", "header")) + xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={ + _mkTag("text", "style-name"): "Header" + }) + xPar.text = self.headerText.strip() + " " + + xTail = etree.SubElement(xPar, _mkTag("text", "page-number"), attrib={ + _mkTag("text", "select-page"): "current" + }) + xTail.text = "2" + + # First Page Header + xHead = etree.SubElement(xPage, _mkTag("style", "header-first")) + xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={ + _mkTag("text", "style-name"): "Header" + }) + return # END Class ToOdt diff --git a/nw/gui/build.py b/nw/gui/build.py index f3d0cad5..a252fc9d 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -365,10 +365,9 @@ class GuiBuildNovel(QDialog): # Build Button # ============ - self.buildProgress = QProgressBar() self.buildProgress = QProgressBar() - self.buildNovel = QPushButton("Build Project") + self.buildNovel = QPushButton("Build Preview") self.buildNovel.clicked.connect(self._buildPreview) # Action Buttons @@ -1155,7 +1154,7 @@ class GuiBuildNovelDocView(QTextBrowser): self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) self.setPlaceholderText( "This area will show the content of the document to be " - "exported or printed. Press the \"Build Project\" button " + "exported or printed. Press the \"Build Preview\" button " "to generate content." ) From f816f3d29b5cccab68bcc472090e98fb01af81b6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Feb 2021 23:28:41 +0100 Subject: [PATCH 062/104] Turn document margins into settings --- nw/core/toodt.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index b45e5c04..ee83673c 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -132,6 +132,12 @@ class ToOdt(Tokenizer): self._mBotText = "0.247cm" self._mBotMeta = "0.106cm" + ## Document Margins + self._mDocTop = "2.000cm" + self._mDocBtm = "2.000cm" + self._mDocLeft = "2.000cm" + self._mDocRight = "2.000cm" + ## Colour self._colHead12 = None self._opaHead12 = None @@ -683,10 +689,6 @@ class ToOdt(Tokenizer): def _pageStyles(self): """Set the default page style. """ - # If we're in flat layout, the page style goes to the main auto-styles - # In archived file, we make a duplicate auto-styles in the self._xPage - # variable which is later added to the styles.xml to go with the - # master-page definition theAttr = {} theAttr[_mkTag("style", "name")] = "PM1" if self._isFlat: @@ -695,10 +697,10 @@ class ToOdt(Tokenizer): xPage = etree.SubElement(self._xAut2, _mkTag("style", "page-layout"), attrib=theAttr) theAttr = {} - theAttr[_mkTag("fo", "margin-top")] = "2.000cm" - theAttr[_mkTag("fo", "margin-bottom")] = "2.000cm" - theAttr[_mkTag("fo", "margin-left")] = "2.000cm" - theAttr[_mkTag("fo", "margin-right")] = "2.000cm" + theAttr[_mkTag("fo", "margin-top")] = self._mDocTop + theAttr[_mkTag("fo", "margin-bottom")] = self._mDocBtm + theAttr[_mkTag("fo", "margin-left")] = self._mDocLeft + theAttr[_mkTag("fo", "margin-right")] = self._mDocRight etree.SubElement(xPage, _mkTag("style", "page-layout-properties"), attrib=theAttr) xHead = etree.SubElement(xPage, _mkTag("style", "header-style")) @@ -707,7 +709,7 @@ class ToOdt(Tokenizer): theAttr[_mkTag("fo", "min-height")] = "0.600cm" theAttr[_mkTag("fo", "margin-left")] = "0.000cm" theAttr[_mkTag("fo", "margin-right")] = "0.000cm" - theAttr[_mkTag("fo", "margin-bottom")] = "0.499cm" + theAttr[_mkTag("fo", "margin-bottom")] = "0.500cm" etree.SubElement(xHead, _mkTag("style", "header-footer-properties"), attrib=theAttr) return From 479975fdff5c3c7b96ef1c2c6365a6c46d1a9244 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 8 Feb 2021 21:33:06 +0100 Subject: [PATCH 063/104] Move clock tick timer to main GUI --- nw/gui/statusbar.py | 15 ++------------- nw/guimain.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index f17af853..5997561d 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -29,7 +29,6 @@ import logging from time import time -from PyQt5.QtCore import QTimer from PyQt5.QtGui import QColor, QPainter from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton @@ -110,12 +109,6 @@ class GuiMainStatus(QStatusBar): # Other Settings self.setSizeGripEnabled(True) - # Start the Clock - self.sessionTimer = QTimer() - self.sessionTimer.setInterval(1000) - self.sessionTimer.timeout.connect(self._updateTime) - self.sessionTimer.start() - logger.debug("GuiMainStatus initialisation complete") self.clearStatus() @@ -130,7 +123,7 @@ class GuiMainStatus(QStatusBar): self.setStats(0, 0) self.setProjectStatus(None) self.setDocumentStatus(None) - self._updateTime() + self.updateTime() return True ## @@ -182,11 +175,7 @@ class GuiMainStatus(QStatusBar): self.statsText.setToolTip("Project word count (session change)") return - ## - # Internal Functions - ## - - def _updateTime(self): + def updateTime(self): """Update the session clock. """ if self.refTime is None: diff --git a/nw/guimain.py b/nw/guimain.py index 1511009e..1e0942c7 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -86,6 +86,8 @@ class GuiMain(QMainWindow): self.theIndex = NWIndex(self.theProject, self) self.hasProject = False self.isFocusMode = False + self.userActive = False + self.lastActive = 0 # Prepare Main Window self.resize(*self.mainConf.getWinSize()) @@ -241,6 +243,12 @@ class GuiMain(QMainWindow): self.asDocTimer = QTimer() self.asDocTimer.timeout.connect(self._autoSaveDocument) + # Main Clock + self.mainTimer = QTimer() + self.mainTimer.setInterval(1000) + self.mainTimer.timeout.connect(self._timeTick) + self.mainTimer.start() + # Shortcuts and Actions self._connectMenuActions() @@ -1414,6 +1422,13 @@ class GuiMain(QMainWindow): # Slots ## + @pyqtSlot() + def _timeTick(self): + """Triggered on every tick of the timer. + """ + self.statusBar.updateTime() + return + @pyqtSlot() def _treeSingleClick(self): """Single click on a project tree item just updates the details From 407f12760cc116a67ad89a322ca3c836aad2ffc5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 8 Feb 2021 21:58:38 +0100 Subject: [PATCH 064/104] Record idle time --- nw/gui/statusbar.py | 4 ++-- nw/guimain.py | 23 +++++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 5997561d..ebc0593a 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -175,13 +175,13 @@ class GuiMainStatus(QStatusBar): self.statsText.setToolTip("Project word count (session change)") return - def updateTime(self): + def updateTime(self, idleTime=0.0): """Update the session clock. """ if self.refTime is None: self.timeText.setText("00:00:00") else: - self.timeText.setText(formatTime(round(time() - self.refTime))) + self.timeText.setText(formatTime(round(time() - self.refTime - idleTime))) return # END Class GuiMainStatus diff --git a/nw/guimain.py b/nw/guimain.py index 1e0942c7..1bf3fd8f 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -86,8 +86,8 @@ class GuiMain(QMainWindow): self.theIndex = NWIndex(self.theProject, self) self.hasProject = False self.isFocusMode = False - self.userActive = False - self.lastActive = 0 + self.idleRefTime = time() + self.idleTime = 0.0 # Prepare Main Window self.resize(*self.mainConf.getWinSize()) @@ -485,7 +485,9 @@ class GuiMain(QMainWindow): return False # Project is loaded - self.hasProject = True + self.hasProject = True + self.idleRefTime = time() + self.idleTime = 0.0 # Load the tag index self.theIndex.loadIndex() @@ -1426,7 +1428,20 @@ class GuiMain(QMainWindow): def _timeTick(self): """Triggered on every tick of the timer. """ - self.statusBar.updateTime() + if not self.hasProject: + return + + currTime = time() + editIdle = currTime - self.docEditor.lastEdit > 30.0 + userIdle = qApp.applicationState() != Qt.ApplicationActive + + if editIdle or userIdle: + self.idleTime += currTime - self.idleRefTime + + self.idleRefTime = currTime + self.statusBar.updateTime(idleTime=self.idleTime) + # print(editIdle, userIdle, self.idleTime) + return @pyqtSlot() From 3521d4a4f73ed7081bb8458271c9548bcd82d887 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 8 Feb 2021 23:17:33 +0100 Subject: [PATCH 065/104] Add idle time to session log and writing stats dialog --- nw/core/options.py | 1 + nw/core/project.py | 13 +++---- nw/gui/writingstats.py | 79 +++++++++++++++++++++++++++++------------- nw/guimain.py | 2 +- 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index c0df1855..b7d0e752 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -47,6 +47,7 @@ class OptionState(): "widthCol0", "widthCol1", "widthCol2", + "widthCol3", "sortCol", "sortOrder", "incNovel", diff --git a/nw/core/project.py b/nw/core/project.py index 90d3649f..fcb675ae 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -726,12 +726,12 @@ class NWProject(): return True - def closeProject(self): + def closeProject(self, idleTime=0): """Close the current project and clear all meta data. """ self.optState.saveSettings() self.projTree.writeToCFile() - self._appendSessionStats() + self._appendSessionStats(idleTime) self._clearLockFile() self.clearProject() self.lockedBy = None @@ -1389,7 +1389,7 @@ class NWProject(): return True - def _appendSessionStats(self): + def _appendSessionStats(self, idleTime): """Append session statistics to the sessions log file. """ if not self.ensureFolderStructure(): @@ -1404,15 +1404,16 @@ class NWProject(): # It's a new file, so add a header if self.lastWCount > 0: outFile.write("# Offset %d\n" % self.lastWCount) - outFile.write("# %-17s %-19s %8s %8s\n" % ( - "Start Time", "End Time", "Novel", "Notes" + outFile.write("# %-17s %-19s %8s %8s %8s\n" % ( + "Start Time", "End Time", "Novel", "Notes", "Idle" )) - outFile.write("%-19s %-19s %8d %8d\n" % ( + outFile.write("%-19s %-19s %8d %8d %8d\n" % ( formatTimeStamp(self.projOpened), formatTimeStamp(time()), self.novelWCount, self.notesWCount, + int(idleTime), )) except Exception: diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 0b5aaabb..f0114b75 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -48,8 +48,9 @@ class GuiWritingStats(QDialog): C_TIME = 0 C_LENGTH = 1 - C_COUNT = 2 - C_BAR = 3 + C_IDLE = 2 + C_COUNT = 3 + C_BAR = 4 FMT_JSON = 0 FMT_CSV = 1 @@ -89,16 +90,21 @@ class GuiWritingStats(QDialog): wCol2 = self.mainConf.pxInt( self.optState.getInt("GuiWritingStats", "widthCol2", 80) ) + wCol3 = self.mainConf.pxInt( + self.optState.getInt("GuiWritingStats", "widthCol3", 80) + ) self.listBox = QTreeWidget() - self.listBox.setHeaderLabels(["Session Start", "Length", "Words", "Histogram"]) + self.listBox.setHeaderLabels(["Session Start", "Length", "Idle", "Words", "Histogram"]) self.listBox.setIndentation(0) self.listBox.setColumnWidth(self.C_TIME, wCol0) self.listBox.setColumnWidth(self.C_LENGTH, wCol1) - self.listBox.setColumnWidth(self.C_COUNT, wCol2) + self.listBox.setColumnWidth(self.C_IDLE, wCol2) + self.listBox.setColumnWidth(self.C_COUNT, wCol3) hHeader = self.listBox.headerItem() hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight) hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) @@ -127,6 +133,10 @@ class GuiWritingStats(QDialog): self.labelTotal.setFont(self.theTheme.guiFontFixed) self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + self.labelIdleT = QLabel(formatTime(0)) + self.labelIdleT.setFont(self.theTheme.guiFontFixed) + self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + self.labelFilter = QLabel(formatTime(0)) self.labelFilter.setFont(self.theTheme.guiFontFixed) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) @@ -144,16 +154,18 @@ class GuiWritingStats(QDialog): self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) - self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) - self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0) - self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0) - self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0) + self.infoForm.addWidget(QLabel("Idle Time:"), 1, 0) + self.infoForm.addWidget(QLabel("Filtered Time:"), 2, 0) + self.infoForm.addWidget(QLabel("Novel Word Count:"), 3, 0) + self.infoForm.addWidget(QLabel("Notes Word Count:"), 4, 0) + self.infoForm.addWidget(QLabel("Total Word Count:"), 5, 0) self.infoForm.addWidget(self.labelTotal, 0, 1) - self.infoForm.addWidget(self.labelFilter, 1, 1) - self.infoForm.addWidget(self.novelWords, 2, 1) - self.infoForm.addWidget(self.notesWords, 3, 1) - self.infoForm.addWidget(self.totalWords, 4, 1) - self.infoForm.setRowStretch(5, 1) + self.infoForm.addWidget(self.labelIdleT, 1, 1) + self.infoForm.addWidget(self.labelFilter, 2, 1) + self.infoForm.addWidget(self.novelWords, 3, 1) + self.infoForm.addWidget(self.notesWords, 4, 1) + self.infoForm.addWidget(self.totalWords, 5, 1) + self.infoForm.setRowStretch(6, 1) # Filter Options sPx = self.theTheme.baseIconSize @@ -278,6 +290,7 @@ class GuiWritingStats(QDialog): widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) + widthCol3 = self.mainConf.rpxInt(self.listBox.columnWidth(3)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() incNovel = self.incNovel.isChecked() @@ -292,6 +305,7 @@ class GuiWritingStats(QDialog): self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0) self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1) self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2) + self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3) self.optState.setValue("GuiWritingStats", "sortCol", sortCol) self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder) self.optState.setValue("GuiWritingStats", "incNovel", incNovel) @@ -347,23 +361,25 @@ class GuiWritingStats(QDialog): with open(savePath, mode="w", encoding="utf8") as outFile: if dataFmt == self.FMT_JSON: jsonData = [] - for _, sD, tT, wD, wA, wB in self.filterData: + for _, sD, tT, wD, wA, wB, tI in self.filterData: jsonData.append({ "date": sD, "length": tT, "newWords": wD, "novelWords": wA, "noteWords": wB, + "idleTime": tI, }) json.dump(jsonData, outFile, indent=2) wSuccess = True if dataFmt == self.FMT_CSV: outFile.write( - '"Date","Length (sec)","Words Changed","Novel Words","Note Words"\n' + '"Date","Length (sec)","Words Changed",' + '"Novel Words","Note Words","Idle Time (sec)"\n' ) - for _, sD, tT, wD, wA, wB in self.filterData: - outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\n') + for _, sD, tT, wD, wA, wB, tI in self.filterData: + outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB},{tI}\n') wSuccess = True except Exception as e: @@ -401,6 +417,7 @@ class GuiWritingStats(QDialog): ttNovel = 0 ttNotes = 0 ttTime = 0 + ttIdle = 0 logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS) if not os.path.isfile(logFile): @@ -419,7 +436,7 @@ class GuiWritingStats(QDialog): continue inData = inLine.split() - if len(inData) != 6: + if len(inData) < 6: continue dStart = datetime.strptime( @@ -429,16 +446,21 @@ class GuiWritingStats(QDialog): "%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP ) + sIdle = 0 + if len(inData) > 6: + sIdle = checkInt(inData[6], 0) + tDiff = dEnd - dStart sDiff = tDiff.total_seconds() ttTime += sDiff + ttIdle += sIdle wcNovel = int(inData[4]) wcNotes = int(inData[5]) ttNovel = wcNovel ttNotes = wcNotes - self.logData.append((dStart, sDiff, wcNovel, wcNotes)) + self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle)) except Exception as e: self.theParent.makeAlert( @@ -448,6 +470,7 @@ class GuiWritingStats(QDialog): ttWords = ttNovel + ttNotes self.labelTotal.setText(formatTime(round(ttTime))) + self.labelIdleT.setText(formatTime(round(ttIdle))) self.novelWords.setText(f"{ttNovel:n}") self.notesWords.setText(f"{ttNotes:n}") self.totalWords.setText(f"{ttWords:n}") @@ -474,25 +497,28 @@ class GuiWritingStats(QDialog): tempData = [] sessDate = None sessTime = 0 + sIdle = 0 lstNovel = 0 lstNotes = 0 - for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData): + for n, (dStart, sDiff, wcNovel, wcNotes, sIdle) in enumerate(self.logData): if n == 0: sessDate = dStart.date() if sessDate != dStart.date(): - tempData.append((sessDate, sessTime, lstNovel, lstNotes)) + tempData.append((sessDate, sessTime, lstNovel, lstNotes, sIdle)) sessDate = dStart.date() sessTime = sDiff + sIdle = sIdle lstNovel = wcNovel lstNotes = wcNotes else: sessTime += sDiff + sIdle += sIdle lstNovel = wcNovel lstNotes = wcNotes if sessDate is not None: - tempData.append((sessDate, sessTime, lstNovel, lstNotes)) + tempData.append((sessDate, sessTime, lstNovel, lstNotes, sIdle)) else: tempData = self.logData @@ -502,7 +528,7 @@ class GuiWritingStats(QDialog): pcTotal = 0 listMax = 0 isFirst = True - for dStart, sDiff, wcNovel, wcNotes in tempData: + for dStart, sDiff, wcNovel, wcNotes, sIdle in tempData: wcTotal = 0 if incNovel: @@ -527,16 +553,17 @@ class GuiWritingStats(QDialog): else: sStart = dStart.strftime(nwConst.FMT_TSTAMP) - self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes)) + self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes, sIdle)) listMax = min(max(listMax, dwTotal), histMax) pcTotal = wcTotal # Populate the list - for _, sStart, sDiff, nWords, _, _ in self.filterData: + for _, sStart, sDiff, nWords, _, _, sIdle in self.filterData: newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, formatTime(round(sDiff))) + newItem.setText(self.C_IDLE, formatTime(sIdle)) newItem.setText(self.C_COUNT, f"{nWords:n}") if nWords > 0 and listMax > 0: @@ -549,11 +576,13 @@ class GuiWritingStats(QDialog): newItem.setData(self.C_BAR, Qt.DecorationRole, theBar) newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + newItem.setTextAlignment(self.C_IDLE, Qt.AlignRight) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed) newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed) + newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed) newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed) self.listBox.addTopLevelItem(newItem) diff --git a/nw/guimain.py b/nw/guimain.py index 1bf3fd8f..816856a0 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -420,7 +420,7 @@ class GuiMain(QMainWindow): self.closeDocument() self.docViewer.clearNavHistory() self.projView.closeOutline() - self.theProject.closeProject() + self.theProject.closeProject(self.idleTime) self.theIndex.clearIndex() self.clearGUI() self.hasProject = False From 871a0dfc6c6e42f1375a74ee95151f6cea54392f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 8 Feb 2021 23:26:40 +0100 Subject: [PATCH 066/104] Fix tests --- tests/test_core/test_core_project.py | 10 +++++----- tests/test_gui/test_gui_writingstats.py | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 61c2d25f..09f6bbb8 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -859,12 +859,12 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir): # Session stats monkeypatch.setattr("os.path.isdir", lambda *args, **kwargs: False) - assert not theProject._appendSessionStats() + assert not theProject._appendSessionStats(idleTime=0) monkeypatch.undo() # Block open monkeypatch.setattr("builtins.open", causeOSError) - assert not theProject._appendSessionStats() + assert not theProject._appendSessionStats(idleTime=0) monkeypatch.undo() # Write entry @@ -876,13 +876,13 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir): theProject.notesWCount = 100 monkeypatch.setattr("nw.core.project.time", lambda: 1600005600) - assert theProject._appendSessionStats() + assert theProject._appendSessionStats(idleTime=99) monkeypatch.undo() assert readFile(statsFile) == ( "# Offset 100\n" - "# Start Time End Time Novel Notes\n" - "%s %s 200 100\n" + "# Start Time End Time Novel Notes Idle\n" + "%s %s 200 100 99\n" ) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600)) # Pack XML Value diff --git a/tests/test_gui/test_gui_writingstats.py b/tests/test_gui/test_gui_writingstats.py index ab86e0d4..15696571 100644 --- a/tests/test_gui/test_gui_writingstats.py +++ b/tests/test_gui/test_gui_writingstats.py @@ -72,7 +72,7 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): # Make a test log file writeFile(sessFile, ( "# Offset 123\n" - "# Start Time End Time Novel Notes\n" + "# Start Time End Time Novel Notes Idle\n" "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n" "2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n" "2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n" @@ -86,8 +86,8 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): # Make sure a faulty file can still be read writeFile(sessFile, ( "# Offset abc123\n" - "# Start Time End Time Novel Notes\n" - "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n" + "# Start Time End Time Novel Notes Idle\n" + "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0 50\n" "2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n" "2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n" "2020-01-06 21:00:00 2020-01-06 21:00:10 125\n" @@ -100,8 +100,8 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): # ============== writeFile(sessFile, ( - "# Start Time End Time Novel Notes\n" - "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n" + "# Start Time End Time Novel Notes Idle\n" + "2020-01-01 21:00:00 2020-01-01 21:00:05 6 0 50\n" "2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n" "2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n" "2020-01-06 21:00:00 2020-01-06 21:00:10 125 5\n" @@ -133,10 +133,17 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): jsonData = json.load(inFile) assert len(jsonData) == 4 + assert jsonData[0]["length"] >= 4.0 + assert jsonData[0]["newWords"] == 6 + assert jsonData[0]["novelWords"] == 6 + assert jsonData[0]["noteWords"] == 0 + assert jsonData[0]["idleTime"] == 50 + assert jsonData[1]["length"] >= 14.0 assert jsonData[1]["newWords"] == 119 assert jsonData[1]["novelWords"] == 125 assert jsonData[1]["noteWords"] == 0 + assert jsonData[1]["idleTime"] == 0 # Test Filters # ============ @@ -160,6 +167,7 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData[0]["newWords"] == 5 assert jsonData[0]["novelWords"] == 125 assert jsonData[0]["noteWords"] == 5 + assert jsonData[0]["idleTime"] == 0 # No Note Files qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) @@ -177,6 +185,7 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData[1]["newWords"] == 119 assert jsonData[1]["novelWords"] == 125 assert jsonData[1]["noteWords"] == 0 + assert jsonData[1]["idleTime"] == 0 # No Negative Entries qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) From 2d754641b534b0aa5ad02a8c9e599afc1613127a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 8 Feb 2021 23:40:21 +0100 Subject: [PATCH 067/104] Add a switch to hide the idle time column --- nw/core/options.py | 1 + nw/gui/writingstats.py | 28 +++++++++++++++++++++++++++- nw/guimain.py | 1 - 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index b7d0e752..c7849bb0 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -55,6 +55,7 @@ class OptionState(): "hideZeros", "hideNegative", "groupByDay", + "showIdleTime", "histMax", }, "GuiDocSplit": { diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index f0114b75..6063ecf1 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -204,17 +204,25 @@ class GuiWritingStats(QDialog): ) self.groupByDay.clicked.connect(self._updateListBox) + self.showIdleTime = QSwitch(width=2*sPx, height=sPx) + self.showIdleTime.setChecked( + self.optState.getBool("GuiWritingStats", "showIdleTime", False) + ) + self.showIdleTime.clicked.connect(self._idleTimeVisibility) + self.filterForm.addWidget(QLabel("Count novel files"), 0, 0) self.filterForm.addWidget(QLabel("Count note files"), 1, 0) self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0) self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0) self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0) + self.filterForm.addWidget(QLabel("Show idle time column"), 5, 0) self.filterForm.addWidget(self.incNovel, 0, 1) self.filterForm.addWidget(self.incNotes, 1, 1) self.filterForm.addWidget(self.hideZeros, 2, 1) self.filterForm.addWidget(self.hideNegative, 3, 1) self.filterForm.addWidget(self.groupByDay, 4, 1) - self.filterForm.setRowStretch(5, 1) + self.filterForm.addWidget(self.showIdleTime, 5, 1) + self.filterForm.setRowStretch(6, 1) # Settings self.histMax = QSpinBox(self) @@ -263,6 +271,9 @@ class GuiWritingStats(QDialog): self.setLayout(self.outerBox) + # Finalise + self._idleTimeVisibility(None) + logger.debug("GuiWritingStats initialisation complete") return @@ -298,8 +309,12 @@ class GuiWritingStats(QDialog): hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() groupByDay = self.groupByDay.isChecked() + showIdleTime = self.showIdleTime.isChecked() histMax = self.histMax.value() + if not showIdleTime: + widthCol2 = 80 + self.optState.setValue("GuiWritingStats", "winWidth", winWidth) self.optState.setValue("GuiWritingStats", "winHeight", winHeight) self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0) @@ -313,6 +328,7 @@ class GuiWritingStats(QDialog): self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros) self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative) self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay) + self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime) self.optState.setValue("GuiWritingStats", "histMax", histMax) self.optState.saveSettings() @@ -477,6 +493,16 @@ class GuiWritingStats(QDialog): return True + ## + # Slots + ## + + def _idleTimeVisibility(self, dummyVar=None): + """ + """ + self.listBox.setColumnHidden(self.C_IDLE, not self.showIdleTime.isChecked()) + return + def _updateListBox(self, dummyVar=None): """Load/reload the content of the list box. The dummyVar variable captures the variable sent from the widgets connecting diff --git a/nw/guimain.py b/nw/guimain.py index 816856a0..37dca06b 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -1440,7 +1440,6 @@ class GuiMain(QMainWindow): self.idleRefTime = currTime self.statusBar.updateTime(idleTime=self.idleTime) - # print(editIdle, userIdle, self.idleTime) return From 09bf2a0fe07f0cffd53fa18a23ba5b98cc7df2d7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 8 Feb 2021 23:51:03 +0100 Subject: [PATCH 068/104] Forgot to consider GUI scale --- nw/gui/writingstats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 6063ecf1..239aa079 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -313,7 +313,7 @@ class GuiWritingStats(QDialog): histMax = self.histMax.value() if not showIdleTime: - widthCol2 = 80 + widthCol2 = self.mainConf.pxInt(80) self.optState.setValue("GuiWritingStats", "winWidth", winWidth) self.optState.setValue("GuiWritingStats", "winHeight", winHeight) From b1eca89a058a884967ad4535ee66400cc0d1b257 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 9 Feb 2021 18:28:22 +0100 Subject: [PATCH 069/104] Switch icon when session timer is paused --- nw/assets/icons/fallback/status_idle-dark.svg | 31 +++++++++++++++++++ nw/assets/icons/fallback/status_idle.svg | 31 +++++++++++++++++++ nw/gui/statusbar.py | 19 +++++++++++- nw/gui/theme.py | 1 + nw/guimain.py | 3 ++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 nw/assets/icons/fallback/status_idle-dark.svg create mode 100644 nw/assets/icons/fallback/status_idle.svg diff --git a/nw/assets/icons/fallback/status_idle-dark.svg b/nw/assets/icons/fallback/status_idle-dark.svg new file mode 100644 index 00000000..b0480586 --- /dev/null +++ b/nw/assets/icons/fallback/status_idle-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/status_idle.svg b/nw/assets/icons/fallback/status_idle.svg new file mode 100644 index 00000000..deafb10d --- /dev/null +++ b/nw/assets/icons/fallback/status_idle.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index ebc0593a..b7723963 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -48,6 +48,7 @@ class GuiMainStatus(QStatusBar): self.theParent = theParent self.theTheme = theParent.theTheme self.refTime = None + self.userIdle = False colNone = QColor(*self.theTheme.statNone) colTrue = QColor(*self.theTheme.statUnsaved) @@ -96,9 +97,12 @@ class GuiMainStatus(QStatusBar): ## The Session Clock ### Set the mimimum width so the label doesn't rescale every second + self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx)) + self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx)) + self.timeIcon = QLabel() self.timeText = QLabel("") - self.timeIcon.setPixmap(self.theTheme.getPixmap("status_time", (iPx, iPx))) + self.timeIcon.setPixmap(self.timePixmap) self.timeText.setToolTip("Session Time") self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:")) self.timeIcon.setContentsMargins(0, 0, 0, 0) @@ -175,6 +179,19 @@ class GuiMainStatus(QStatusBar): self.statsText.setToolTip("Project word count (session change)") return + def setUserIdle(self, userIdle): + """Change the idle status icon. + """ + if self.userIdle != userIdle: + if userIdle: + self.timeIcon.setPixmap(self.idlePixmap) + else: + self.timeIcon.setPixmap(self.timePixmap) + + self.userIdle = userIdle + + return + def updateTime(self, idleTime=0.0): """Update the session clock. """ diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 14ce5ec3..9ec6018e 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -539,6 +539,7 @@ class GuiIcons: "proj_nwx" : (None, None), "status_lang" : (None, None), "status_time" : (None, None), + "status_idle" : (None, None), "status_stats" : (None, None), "status_lines" : (None, None), "doc_h0" : (QStyle.SP_FileIcon, "x-office-document"), diff --git a/nw/guimain.py b/nw/guimain.py index 37dca06b..344bafbd 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -1437,6 +1437,9 @@ class GuiMain(QMainWindow): if editIdle or userIdle: self.idleTime += currTime - self.idleRefTime + self.statusBar.setUserIdle(True) + else: + self.statusBar.setUserIdle(False) self.idleRefTime = currTime self.statusBar.updateTime(idleTime=self.idleTime) From f1b31f26d07211bcb7d4d3d593b1a9d813cda59f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 9 Feb 2021 18:54:44 +0100 Subject: [PATCH 070/104] Add preferences for controlling session timer behaviour --- nw/config.py | 11 +++++++++++ nw/gui/doceditor.py | 25 +++++++++++++++---------- nw/gui/preferences.py | 31 +++++++++++++++++++++++++++++++ nw/gui/statusbar.py | 9 ++++++++- nw/guimain.py | 6 +++++- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/nw/config.py b/nw/config.py index 028574ca..58229ea5 100644 --- a/nw/config.py +++ b/nw/config.py @@ -144,6 +144,9 @@ class Config: self.allowOpenDQuote = True # Allow open-ended double quotes self.highlightEmph = True # Add colour to text emphasis + self.stopWhenIdle = True # Stop the status bar clock when the user is idle + self.userIdleTime = 300 # Time of inactivity to consider user idle + ## User-Selected Symbols self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] @@ -532,6 +535,12 @@ class Config: self.highlightEmph = self._parseLine( cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph ) + self.stopWhenIdle = self._parseLine( + cnfParse, cnfSec, "stopwhenidle", self.CNF_BOOL, self.stopWhenIdle + ) + self.userIdleTime = self._parseLine( + cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime + ) ## Backup cnfSec = "Backup" @@ -672,6 +681,8 @@ class Config: cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote)) cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote)) cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph)) + cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle)) + cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime)) ## Backup cnfSec = "Backup" diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9ebb400a..abafc6fb 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -91,6 +91,7 @@ class GuiDocEditor(QTextEdit): self.wordCount = 0 # Word count self.paraCount = 0 # Paragraph count self.lastEdit = 0 # Time stamp of last edit + self.lastActive = 0 # Time stamp of last activity self.lastFind = None # Position of the last found search word self.bigDoc = False # Flag for very large document size self.doReplace = False # Switch to temporarily disable auto-replace @@ -169,15 +170,16 @@ class GuiDocEditor(QTextEdit): self.clear() self.wcTimer.stop() - self.theHandle = None - self.charCount = 0 - self.wordCount = 0 - self.paraCount = 0 - self.lastEdit = 0 - self.lastFind = None - self.bigDoc = False - self.doReplace = False - self.queuePos = None + self.theHandle = None + self.charCount = 0 + self.wordCount = 0 + self.paraCount = 0 + self.lastEdit = 0 + self.lastActive = 0 + self.lastFind = None + self.bigDoc = False + self.doReplace = False + self.queuePos = None self.setDocumentChanged(False) self.docHeader.setTitleFromHandle(self.theHandle) @@ -319,6 +321,7 @@ class GuiDocEditor(QTextEdit): logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime))) self.lastEdit = time() + self.lastActive = time() self._runCounter() self.wcTimer.start() self.theHandle = tHandle @@ -722,6 +725,7 @@ class GuiDocEditor(QTextEdit): return False self._allowAutoReplace(True) + self.lastActive = time() return True @@ -839,6 +843,7 @@ class GuiDocEditor(QTextEdit): * The undo/redo/select all sequences bypasses the docAction pathway from the menu, so we redirect them back from here. """ + self.lastActive = time() isReturn = keyEvent.key() == Qt.Key_Return isReturn |= keyEvent.key() == Qt.Key_Enter if isReturn and self.docSearch.anyFocus(): @@ -1083,7 +1088,7 @@ class GuiDocEditor(QTextEdit): logger.verbose("Word counter is busy") return - if time() - self.lastEdit < 5*self.wcInterval: + if time() - self.lastEdit < 5 * self.wcInterval: logger.verbose("Running word counter") self.theParent.threadPool.start(self.wCounter) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index b7be1d05..4913be25 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -358,6 +358,33 @@ class GuiPreferencesProjects(QWidget): "If off, backups will run in the background." ) + # Session Timer + # ============= + self.mainForm.addGroupLabel("Session Timer") + + ## Pause when idle + self.stopWhenIdle = QSwitch() + self.stopWhenIdle.setChecked(self.mainConf.stopWhenIdle) + self.mainForm.addRow( + "Pause the session timer when not writing", + self.stopWhenIdle, + "Also pauses when the application window does not have focus." + ) + + ## Inactive time for idle + self.userIdleTime = QDoubleSpinBox() + self.userIdleTime.setMinimum(0.5) + self.userIdleTime.setMaximum(600.0) + self.userIdleTime.setSingleStep(0.5) + self.userIdleTime.setDecimals(1) + self.userIdleTime.setValue(self.mainConf.userIdleTime/60.0) + self.mainForm.addRow( + "Editor inactive time before pausing timer", + self.userIdleTime, + "User activity includes typing and changing the content.", + theUnit="minutes" + ) + return def saveValues(self): @@ -372,6 +399,10 @@ class GuiPreferencesProjects(QWidget): self.mainConf.backupOnClose = self.backupOnClose.isChecked() self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked() + # Session Timer + self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked() + self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60) + self.mainConf.confChanged = True return diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index b7723963..441cacda 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -182,6 +182,9 @@ class GuiMainStatus(QStatusBar): def setUserIdle(self, userIdle): """Change the idle status icon. """ + if not self.mainConf.stopWhenIdle: + userIdle = False + if self.userIdle != userIdle: if userIdle: self.timeIcon.setPixmap(self.idlePixmap) @@ -198,7 +201,11 @@ class GuiMainStatus(QStatusBar): if self.refTime is None: self.timeText.setText("00:00:00") else: - self.timeText.setText(formatTime(round(time() - self.refTime - idleTime))) + if self.mainConf.stopWhenIdle: + sessTime = round(time() - self.refTime - idleTime) + else: + sessTime = round(time() - self.refTime) + self.timeText.setText(formatTime(sessTime)) return # END Class GuiMainStatus diff --git a/nw/guimain.py b/nw/guimain.py index 344bafbd..c72c6d46 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -420,7 +420,11 @@ class GuiMain(QMainWindow): self.closeDocument() self.docViewer.clearNavHistory() self.projView.closeOutline() + self.theProject.closeProject(self.idleTime) + self.idleRefTime = time() + self.idleTime = 0.0 + self.theIndex.clearIndex() self.clearGUI() self.hasProject = False @@ -1432,7 +1436,7 @@ class GuiMain(QMainWindow): return currTime = time() - editIdle = currTime - self.docEditor.lastEdit > 30.0 + editIdle = currTime - self.docEditor.lastActive > self.mainConf.userIdleTime userIdle = qApp.applicationState() != Qt.ApplicationActive if editIdle or userIdle: From 2439cf4633cca18ab61acfa17f4a56f44c20d88d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 9 Feb 2021 19:46:00 +0100 Subject: [PATCH 071/104] Writing Stats tool should show idle time as either percantage or actual time --- nw/gui/writingstats.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 239aa079..34870c20 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -208,7 +208,7 @@ class GuiWritingStats(QDialog): self.showIdleTime.setChecked( self.optState.getBool("GuiWritingStats", "showIdleTime", False) ) - self.showIdleTime.clicked.connect(self._idleTimeVisibility) + self.showIdleTime.clicked.connect(self._updateListBox) self.filterForm.addWidget(QLabel("Count novel files"), 0, 0) self.filterForm.addWidget(QLabel("Count note files"), 1, 0) @@ -271,9 +271,6 @@ class GuiWritingStats(QDialog): self.setLayout(self.outerBox) - # Finalise - self._idleTimeVisibility(None) - logger.debug("GuiWritingStats initialisation complete") return @@ -312,9 +309,6 @@ class GuiWritingStats(QDialog): showIdleTime = self.showIdleTime.isChecked() histMax = self.histMax.value() - if not showIdleTime: - widthCol2 = self.mainConf.pxInt(80) - self.optState.setValue("GuiWritingStats", "winWidth", winWidth) self.optState.setValue("GuiWritingStats", "winHeight", winHeight) self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0) @@ -497,12 +491,6 @@ class GuiWritingStats(QDialog): # Slots ## - def _idleTimeVisibility(self, dummyVar=None): - """ - """ - self.listBox.setColumnHidden(self.C_IDLE, not self.showIdleTime.isChecked()) - return - def _updateListBox(self, dummyVar=None): """Load/reload the content of the list box. The dummyVar variable captures the variable sent from the widgets connecting @@ -584,12 +572,19 @@ class GuiWritingStats(QDialog): pcTotal = wcTotal # Populate the list + showIdleTime = self.showIdleTime.isChecked() for _, sStart, sDiff, nWords, _, _, sIdle in self.filterData: + if showIdleTime: + idleEntry = formatTime(sIdle) + else: + sRatio = sIdle/sDiff if sDiff > 0.0 else 0.0 + idleEntry = "%d %%" % round(100.0 * sRatio) + newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, formatTime(round(sDiff))) - newItem.setText(self.C_IDLE, formatTime(sIdle)) + newItem.setText(self.C_IDLE, idleEntry) newItem.setText(self.C_COUNT, f"{nWords:n}") if nWords > 0 and listMax > 0: @@ -608,8 +603,11 @@ class GuiWritingStats(QDialog): newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed) newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed) - newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed) newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed) + if showIdleTime: + newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed) + else: + newItem.setFont(self.C_IDLE, self.theTheme.guiFont) self.listBox.addTopLevelItem(newItem) self.timeFilter += sDiff From 6904afc5e7414ee325db730e2c86e6112e292c84 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 9 Feb 2021 21:09:03 +0100 Subject: [PATCH 072/104] Updated tests --- tests/reference/baseConfig_novelwriter.conf | 6 +- .../reference/guiPreferences_novelwriter.conf | 16 +-- tests/test_gui/test_gui_writingstats.py | 114 +++++++++--------- 3 files changed, 70 insertions(+), 66 deletions(-) diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 588d5263..1ab78ea7 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -1,12 +1,12 @@ [Main] -timestamp = 2020-10-11 22:50:45 +timestamp = 2021-02-09 21:07:03 theme = default syntax = default_light icons = typicons_colour_light guidark = False guifont = guifontsize = 11 -lastnotes = 1.0 +lastnotes = 0x0 [Sizes] geometry = 1200, 650 @@ -56,6 +56,8 @@ highlightquotes = True allowopensquote = False allowopendquote = True highlightemph = True +stopwhenidle = True +useridletime = 300 [Backup] backuppath = diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index db78d507..f440e00b 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -1,18 +1,18 @@ [Main] -timestamp = 2020-06-29 17:34:15 +timestamp = 2021-02-09 21:07:17 theme = default syntax = default_light icons = typicons_colour_light guidark = True -guifont = Cantarell +guifont = Sans guifontsize = 12 -lastnotes = 1.0 +lastnotes = 0x0 [Sizes] -geometry = 1100, 650 -treecols = 120, 30, 50 +geometry = 1200, 650 +treecols = 200, 50, 30 novelcols = 200, 50 -projcols = 140, 55, 140 +projcols = 200, 60, 140 mainpane = 300, 800 docpane = 400, 400 viewpane = 500, 150 @@ -26,7 +26,7 @@ autosaveproject = 40 autosavedoc = 20 [Editor] -textfont = Cantarell +textfont = None textsize = 13 fixedwidth = False width = 700 @@ -56,6 +56,8 @@ highlightquotes = False allowopensquote = False allowopendquote = True highlightemph = False +stopwhenidle = True +useridletime = 300 [Backup] backuppath = some/dir diff --git a/tests/test_gui/test_gui_writingstats.py b/tests/test_gui/test_gui_writingstats.py index 071ea9f7..c96bf14d 100644 --- a/tests/test_gui/test_gui_writingstats.py +++ b/tests/test_gui/test_gui_writingstats.py @@ -101,17 +101,17 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): writeFile(sessFile, ( "# Offset 1075\n" - "# Start Time End Time Novel Notes\n" - "2021-01-31 19:00:00 2021-01-31 19:30:00 700 375\n" - "2021-02-01 19:00:00 2021-02-01 19:30:00 700 375\n" - "2021-02-01 20:00:00 2021-02-01 20:30:00 600 275\n" - "2021-02-02 19:00:00 2021-02-02 19:30:00 750 425\n" - "2021-02-02 20:00:00 2021-02-02 20:30:00 690 365\n" - "2021-02-03 19:00:00 2021-02-03 19:30:00 680 355\n" - "2021-02-04 19:00:00 2021-02-04 19:30:00 700 375\n" - "2021-02-05 19:00:00 2021-02-05 19:30:00 500 175\n" - "2021-02-06 19:00:00 2021-02-06 19:30:00 600 275\n" - "2021-02-07 19:00:00 2021-02-07 19:30:00 600 275\n" + "# Start Time End Time Novel Notes Idle\n" + "2021-01-31 19:00:00 2021-01-31 19:30:00 700 375 0\n" + "2021-02-01 19:00:00 2021-02-01 19:30:00 700 375 10\n" + "2021-02-01 20:00:00 2021-02-01 20:30:00 600 275 20\n" + "2021-02-02 19:00:00 2021-02-02 19:30:00 750 425 30\n" + "2021-02-02 20:00:00 2021-02-02 20:30:00 690 365 40\n" + "2021-02-03 19:00:00 2021-02-03 19:30:00 680 355 50\n" + "2021-02-04 19:00:00 2021-02-04 19:30:00 700 375 60\n" + "2021-02-05 19:00:00 2021-02-05 19:30:00 500 175 70\n" + "2021-02-06 19:00:00 2021-02-06 19:30:00 600 275 80\n" + "2021-02-07 19:00:00 2021-02-07 19:30:00 600 275 90\n" )) sessLog.populateGUI() @@ -156,28 +156,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData == [ { "date": "2021-01-31 19:00:00", "length": 1800.0, - "newWords": 1, "novelWords": 700, "noteWords": 375 + "newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0 }, { "date": "2021-02-01 20:00:00", "length": 1800.0, - "newWords": -200, "novelWords": 600, "noteWords": 275 + "newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 20 }, { "date": "2021-02-02 19:00:00", "length": 1800.0, - "newWords": 300, "novelWords": 750, "noteWords": 425 + "newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30 }, { "date": "2021-02-02 20:00:00", "length": 1800.0, - "newWords": -120, "novelWords": 690, "noteWords": 365 + "newWords": -120, "novelWords": 690, "noteWords": 365, "idleTime": 40 }, { "date": "2021-02-03 19:00:00", "length": 1800.0, - "newWords": -20, "novelWords": 680, "noteWords": 355 + "newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50 }, { "date": "2021-02-04 19:00:00", "length": 1800.0, - "newWords": 40, "novelWords": 700, "noteWords": 375 + "newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60 }, { "date": "2021-02-05 19:00:00", "length": 1800.0, - "newWords": -400, "novelWords": 500, "noteWords": 175 + "newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70 }, { "date": "2021-02-06 19:00:00", "length": 1800.0, - "newWords": 200, "novelWords": 600, "noteWords": 275 + "newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80 } ] @@ -206,28 +206,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData == [ { "date": "2021-01-31 19:00:00", "length": 1800.0, - "newWords": 1, "novelWords": 700, "noteWords": 375 + "newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0 }, { "date": "2021-02-01 20:00:00", "length": 1800.0, - "newWords": -100, "novelWords": 600, "noteWords": 275 + "newWords": -100, "novelWords": 600, "noteWords": 275, "idleTime": 20 }, { "date": "2021-02-02 19:00:00", "length": 1800.0, - "newWords": 150, "novelWords": 750, "noteWords": 425 + "newWords": 150, "novelWords": 750, "noteWords": 425, "idleTime": 30 }, { "date": "2021-02-02 20:00:00", "length": 1800.0, - "newWords": -60, "novelWords": 690, "noteWords": 365 + "newWords": -60, "novelWords": 690, "noteWords": 365, "idleTime": 40 }, { "date": "2021-02-03 19:00:00", "length": 1800.0, - "newWords": -10, "novelWords": 680, "noteWords": 355 + "newWords": -10, "novelWords": 680, "noteWords": 355, "idleTime": 50 }, { "date": "2021-02-04 19:00:00", "length": 1800.0, - "newWords": 20, "novelWords": 700, "noteWords": 375 + "newWords": 20, "novelWords": 700, "noteWords": 375, "idleTime": 60 }, { "date": "2021-02-05 19:00:00", "length": 1800.0, - "newWords": -200, "novelWords": 500, "noteWords": 175 + "newWords": -200, "novelWords": 500, "noteWords": 175, "idleTime": 70 }, { "date": "2021-02-06 19:00:00", "length": 1800.0, - "newWords": 100, "novelWords": 600, "noteWords": 275 + "newWords": 100, "novelWords": 600, "noteWords": 275, "idleTime": 80 } ] @@ -254,28 +254,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData == [ { "date": "2021-01-31 19:00:00", "length": 1800.0, - "newWords": 1, "novelWords": 700, "noteWords": 375 + "newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0 }, { "date": "2021-02-01 20:00:00", "length": 1800.0, - "newWords": -100, "novelWords": 600, "noteWords": 275 + "newWords": -100, "novelWords": 600, "noteWords": 275, "idleTime": 20 }, { "date": "2021-02-02 19:00:00", "length": 1800.0, - "newWords": 150, "novelWords": 750, "noteWords": 425 + "newWords": 150, "novelWords": 750, "noteWords": 425, "idleTime": 30 }, { "date": "2021-02-02 20:00:00", "length": 1800.0, - "newWords": -60, "novelWords": 690, "noteWords": 365 + "newWords": -60, "novelWords": 690, "noteWords": 365, "idleTime": 40 }, { "date": "2021-02-03 19:00:00", "length": 1800.0, - "newWords": -10, "novelWords": 680, "noteWords": 355 + "newWords": -10, "novelWords": 680, "noteWords": 355, "idleTime": 50 }, { "date": "2021-02-04 19:00:00", "length": 1800.0, - "newWords": 20, "novelWords": 700, "noteWords": 375 + "newWords": 20, "novelWords": 700, "noteWords": 375, "idleTime": 60 }, { "date": "2021-02-05 19:00:00", "length": 1800.0, - "newWords": -200, "novelWords": 500, "noteWords": 175 + "newWords": -200, "novelWords": 500, "noteWords": 175, "idleTime": 70 }, { "date": "2021-02-06 19:00:00", "length": 1800.0, - "newWords": 100, "novelWords": 600, "noteWords": 275 + "newWords": 100, "novelWords": 600, "noteWords": 275, "idleTime": 80 } ] @@ -300,16 +300,16 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData == [ { "date": "2021-01-31 19:00:00", "length": 1800.0, - "newWords": 1, "novelWords": 700, "noteWords": 375 + "newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0 }, { "date": "2021-02-02 19:00:00", "length": 1800.0, - "newWords": 300, "novelWords": 750, "noteWords": 425 + "newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30 }, { "date": "2021-02-04 19:00:00", "length": 1800.0, - "newWords": 40, "novelWords": 700, "noteWords": 375 + "newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60 }, { "date": "2021-02-06 19:00:00", "length": 1800.0, - "newWords": 200, "novelWords": 600, "noteWords": 275 + "newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80 } ] @@ -338,34 +338,34 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData == [ { "date": "2021-01-31 19:00:00", "length": 1800.0, - "newWords": 1, "novelWords": 700, "noteWords": 375 + "newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0 }, { "date": "2021-02-01 19:00:00", "length": 1800.0, - "newWords": 0, "novelWords": 700, "noteWords": 375 + "newWords": 0, "novelWords": 700, "noteWords": 375, "idleTime": 10 }, { "date": "2021-02-01 20:00:00", "length": 1800.0, - "newWords": -200, "novelWords": 600, "noteWords": 275 + "newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 20 }, { "date": "2021-02-02 19:00:00", "length": 1800.0, - "newWords": 300, "novelWords": 750, "noteWords": 425 + "newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30 }, { "date": "2021-02-02 20:00:00", "length": 1800.0, - "newWords": -120, "novelWords": 690, "noteWords": 365 + "newWords": -120, "novelWords": 690, "noteWords": 365, "idleTime": 40 }, { "date": "2021-02-03 19:00:00", "length": 1800.0, - "newWords": -20, "novelWords": 680, "noteWords": 355 + "newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50 }, { "date": "2021-02-04 19:00:00", "length": 1800.0, - "newWords": 40, "novelWords": 700, "noteWords": 375 + "newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60 }, { "date": "2021-02-05 19:00:00", "length": 1800.0, - "newWords": -400, "novelWords": 500, "noteWords": 175 + "newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70 }, { "date": "2021-02-06 19:00:00", "length": 1800.0, - "newWords": 200, "novelWords": 600, "noteWords": 275 + "newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80 }, { "date": "2021-02-07 19:00:00", "length": 1800.0, - "newWords": 0, "novelWords": 600, "noteWords": 275 + "newWords": 0, "novelWords": 600, "noteWords": 275, "idleTime": 90 } ] @@ -391,28 +391,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert jsonData == [ { "date": "2021-01-31", "length": 1800.0, - "newWords": 1, "novelWords": 700, "noteWords": 375 + "newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 10 }, { "date": "2021-02-01", "length": 3600.0, - "newWords": -200, "novelWords": 600, "noteWords": 275 + "newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 30 }, { "date": "2021-02-02", "length": 3600.0, - "newWords": 180, "novelWords": 690, "noteWords": 365 + "newWords": 180, "novelWords": 690, "noteWords": 365, "idleTime": 50 }, { "date": "2021-02-03", "length": 1800.0, - "newWords": -20, "novelWords": 680, "noteWords": 355 + "newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 60 }, { "date": "2021-02-04", "length": 1800.0, - "newWords": 40, "novelWords": 700, "noteWords": 375 + "newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 70 }, { "date": "2021-02-05", "length": 1800.0, - "newWords": -400, "novelWords": 500, "noteWords": 175 + "newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 80 }, { "date": "2021-02-06", "length": 1800.0, - "newWords": 200, "novelWords": 600, "noteWords": 275 + "newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 90 }, { "date": "2021-02-07", "length": 1800.0, - "newWords": 0, "novelWords": 600, "noteWords": 275 + "newWords": 0, "novelWords": 600, "noteWords": 275, "idleTime": 90 } ] From 78a58617c1340618eb3d2e9ba8d97e223560e100 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Feb 2021 10:38:22 +0100 Subject: [PATCH 073/104] Connect line height to Build dialog --- nw/core/options.py | 1 + nw/gui/build.py | 141 +++++++++++++++++++++++++-------------------- 2 files changed, 79 insertions(+), 63 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index c7849bb0..c47973e8 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -73,6 +73,7 @@ class OptionState(): "excludeBody", "textFont", "textSize", + "lineHeight", "noStyling", "incSynopsis", "incComments", diff --git a/nw/gui/build.py b/nw/gui/build.py index a252fc9d..14ffa0d1 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -41,7 +41,7 @@ from PyQt5.QtWidgets import ( qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFileDialog, QFontDialog, QSpinBox, QScrollArea, QSplitter, QWidget, - QSizePolicy + QSizePolicy, QDoubleSpinBox ) from nw.common import fuzzyTime, makeFileNameSafe @@ -164,23 +164,29 @@ class GuiBuildNovel(QDialog): self.boxTitle.addWidget(self.fmtTitle) self.boxChapter = QHBoxLayout() self.boxChapter.addWidget(self.fmtChapter) - self.boxUnnumbered = QHBoxLayout() - self.boxUnnumbered.addWidget(self.fmtUnnumbered) + self.boxUnnumb = QHBoxLayout() + self.boxUnnumb.addWidget(self.fmtUnnumbered) self.boxScene = QHBoxLayout() self.boxScene.addWidget(self.fmtScene) self.boxSection = QHBoxLayout() self.boxSection.addWidget(self.fmtSection) - self.titleForm.addWidget(QLabel("Title"), 0, 0, 1, 1, Qt.AlignLeft) - self.titleForm.addLayout(self.boxTitle, 0, 1, 1, 1, Qt.AlignRight) - self.titleForm.addWidget(QLabel("Chapter"), 1, 0, 1, 1, Qt.AlignLeft) - self.titleForm.addLayout(self.boxChapter, 1, 1, 1, 1, Qt.AlignRight) - self.titleForm.addWidget(QLabel("Unnumbered"), 2, 0, 1, 1, Qt.AlignLeft) - self.titleForm.addLayout(self.boxUnnumbered, 2, 1, 1, 1, Qt.AlignRight) - self.titleForm.addWidget(QLabel("Scene"), 3, 0, 1, 1, Qt.AlignLeft) - self.titleForm.addLayout(self.boxScene, 3, 1, 1, 1, Qt.AlignRight) - self.titleForm.addWidget(QLabel("Section"), 4, 0, 1, 1, Qt.AlignLeft) - self.titleForm.addLayout(self.boxSection, 4, 1, 1, 1, Qt.AlignRight) + titleLabel = QLabel("Title") + chapterLabel = QLabel("Chapter") + unnumbLabel = QLabel("Unnumbered") + sceneLabel = QLabel("Scene") + sectionLabel = QLabel("Section") + + self.titleForm.addWidget(titleLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.titleForm.addLayout(self.boxTitle, 0, 1, 1, 1, Qt.AlignRight) + self.titleForm.addWidget(chapterLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.titleForm.addLayout(self.boxChapter, 1, 1, 1, 1, Qt.AlignRight) + self.titleForm.addWidget(unnumbLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.titleForm.addLayout(self.boxUnnumb, 2, 1, 1, 1, Qt.AlignRight) + self.titleForm.addWidget(sceneLabel, 3, 0, 1, 1, Qt.AlignLeft) + self.titleForm.addLayout(self.boxScene, 3, 1, 1, 1, Qt.AlignRight) + self.titleForm.addWidget(sectionLabel, 4, 0, 1, 1, Qt.AlignLeft) + self.titleForm.addLayout(self.boxSection, 4, 1, 1, 1, Qt.AlignRight) self.titleForm.setColumnStretch(0, 0) self.titleForm.setColumnStretch(1, 1) @@ -204,29 +210,30 @@ class GuiBuildNovel(QDialog): self.fontButton.clicked.connect(self._selectFont) self.textSize = QSpinBox(self) - self.textSize.setFixedWidth(5*self.theTheme.textNWidth) + self.textSize.setFixedWidth(6*self.theTheme.textNWidth) self.textSize.setMinimum(6) self.textSize.setMaximum(72) self.textSize.setSingleStep(1) - self.textSize.setToolTip( - "The size is used for PDF and printing. Other formats have no size set." - ) self.textSize.setValue( self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize) ) - self.justifyText = QSwitch() - self.justifyText.setToolTip( - "Applies to PDF, printing, HTML, and Open Document exports." + self.lineHeight = QDoubleSpinBox(self) + self.lineHeight.setFixedWidth(6*self.theTheme.textNWidth) + self.lineHeight.setMinimum(0.8) + self.lineHeight.setMaximum(3.0) + self.lineHeight.setSingleStep(0.05) + self.lineHeight.setDecimals(2) + self.lineHeight.setValue( + self.optState.getInt("GuiBuildNovel", "lineHeight", 1.15) ) + + self.justifyText = QSwitch() self.justifyText.setChecked( self.optState.getBool("GuiBuildNovel", "justifyText", False) ) self.noStyling = QSwitch() - self.noStyling.setToolTip( - "Disable all styling of the text." - ) self.noStyling.setChecked( self.optState.getBool("GuiBuildNovel", "noStyling", False) ) @@ -235,15 +242,23 @@ class GuiBuildNovel(QDialog): self.boxFont = QHBoxLayout() self.boxFont.addWidget(self.textFont) - self.formatForm.addWidget(QLabel("Font family"), 0, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight) - self.formatForm.addWidget(self.fontButton, 0, 2, 1, 1, Qt.AlignRight) - self.formatForm.addWidget(QLabel("Font size"), 1, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.textSize, 1, 1, 1, 2, Qt.AlignRight) - self.formatForm.addWidget(QLabel("Justify text"), 2, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.justifyText, 2, 1, 1, 2, Qt.AlignRight) - self.formatForm.addWidget(QLabel("Disable styling"), 3, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.noStyling, 3, 1, 1, 2, Qt.AlignRight) + fontFamilyLabel = QLabel("Font family") + fontSizeLabel = QLabel("Font size") + lineHeightLabel = QLabel("Line height") + justifyLabel = QLabel("Justify text") + stylingLabel = QLabel("Disable styling") + + self.formatForm.addWidget(fontFamilyLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.formatForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight) + self.formatForm.addWidget(self.fontButton, 0, 2, 1, 1, Qt.AlignRight) + self.formatForm.addWidget(fontSizeLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.formatForm.addWidget(self.textSize, 1, 1, 1, 2, Qt.AlignRight) + self.formatForm.addWidget(lineHeightLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.formatForm.addWidget(self.lineHeight, 2, 1, 1, 2, Qt.AlignRight) + self.formatForm.addWidget(justifyLabel, 3, 0, 1, 1, Qt.AlignLeft) + self.formatForm.addWidget(self.justifyText, 3, 1, 1, 2, Qt.AlignRight) + self.formatForm.addWidget(stylingLabel, 4, 0, 1, 1, Qt.AlignLeft) + self.formatForm.addWidget(self.noStyling, 4, 1, 1, 2, Qt.AlignRight) self.formatForm.setColumnStretch(0, 0) self.formatForm.setColumnStretch(1, 1) @@ -257,45 +272,38 @@ class GuiBuildNovel(QDialog): self.textGroup.setLayout(self.textForm) self.includeSynopsis = QSwitch() - self.includeSynopsis.setToolTip( - "Include synopsis comments in the output." - ) self.includeSynopsis.setChecked( self.optState.getBool("GuiBuildNovel", "incSynopsis", False) ) self.includeComments = QSwitch() - self.includeComments.setToolTip( - "Include plain comments in the output." - ) self.includeComments.setChecked( self.optState.getBool("GuiBuildNovel", "incComments", False) ) self.includeKeywords = QSwitch() - self.includeKeywords.setToolTip( - "Include meta keywords (tags, references) in the output." - ) self.includeKeywords.setChecked( self.optState.getBool("GuiBuildNovel", "incKeywords", False) ) self.includeBody = QSwitch() - self.includeBody.setToolTip( - "Include body text in the output." - ) self.includeBody.setChecked( self.optState.getBool("GuiBuildNovel", "incBodyText", True) ) - self.textForm.addWidget(QLabel("Include synopsis"), 0, 0, 1, 1, Qt.AlignLeft) - self.textForm.addWidget(self.includeSynopsis, 0, 1, 1, 1, Qt.AlignRight) - self.textForm.addWidget(QLabel("Include comments"), 1, 0, 1, 1, Qt.AlignLeft) - self.textForm.addWidget(self.includeComments, 1, 1, 1, 1, Qt.AlignRight) - self.textForm.addWidget(QLabel("Include keywords"), 2, 0, 1, 1, Qt.AlignLeft) - self.textForm.addWidget(self.includeKeywords, 2, 1, 1, 1, Qt.AlignRight) - self.textForm.addWidget(QLabel("Include body text"), 3, 0, 1, 1, Qt.AlignLeft) - self.textForm.addWidget(self.includeBody, 3, 1, 1, 1, Qt.AlignRight) + synopsisLabel = QLabel("Include synopsis") + commentsLabel = QLabel("Include comments") + keywordsLabel = QLabel("Include keywords") + bodyLabel = QLabel("Include body text") + + self.textForm.addWidget(synopsisLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.textForm.addWidget(self.includeSynopsis, 0, 1, 1, 1, Qt.AlignRight) + self.textForm.addWidget(commentsLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.textForm.addWidget(self.includeComments, 1, 1, 1, 1, Qt.AlignRight) + self.textForm.addWidget(keywordsLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.textForm.addWidget(self.includeKeywords, 2, 1, 1, 1, Qt.AlignRight) + self.textForm.addWidget(bodyLabel, 3, 0, 1, 1, Qt.AlignLeft) + self.textForm.addWidget(self.includeBody, 3, 1, 1, 1, Qt.AlignRight) self.textForm.setColumnStretch(0, 1) self.textForm.setColumnStretch(1, 0) @@ -331,12 +339,16 @@ class GuiBuildNovel(QDialog): self.optState.getBool("GuiBuildNovel", "ignoreFlag", False) ) - self.fileForm.addWidget(QLabel("Include novel files"), 0, 0, 1, 1, Qt.AlignLeft) - self.fileForm.addWidget(self.novelFiles, 0, 1, 1, 1, Qt.AlignRight) - self.fileForm.addWidget(QLabel("Include note files"), 1, 0, 1, 1, Qt.AlignLeft) - self.fileForm.addWidget(self.noteFiles, 1, 1, 1, 1, Qt.AlignRight) - self.fileForm.addWidget(QLabel("Ignore export flag"), 2, 0, 1, 1, Qt.AlignLeft) - self.fileForm.addWidget(self.ignoreFlag, 2, 1, 1, 1, Qt.AlignRight) + novelLabel = QLabel("Include novel files") + notesLabel = QLabel("Include note files") + exportLabel = QLabel("Ignore export flag") + + self.fileForm.addWidget(novelLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.fileForm.addWidget(self.novelFiles, 0, 1, 1, 1, Qt.AlignRight) + self.fileForm.addWidget(notesLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.fileForm.addWidget(self.noteFiles, 1, 1, 1, 1, Qt.AlignRight) + self.fileForm.addWidget(exportLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.fileForm.addWidget(self.ignoreFlag, 2, 1, 1, 1, Qt.AlignRight) self.fileForm.setColumnStretch(0, 1) self.fileForm.setColumnStretch(1, 0) @@ -349,15 +361,14 @@ class GuiBuildNovel(QDialog): self.exportGroup.setLayout(self.exportForm) self.replaceTabs = QSwitch() - self.replaceTabs.setToolTip( - "Replace all tabs with eight spaces." - ) self.replaceTabs.setChecked( self.optState.getBool("GuiBuildNovel", "replaceTabs", False) ) - self.exportForm.addWidget(QLabel("Replace tabs with spaces"), 0, 0, 1, 1, Qt.AlignLeft) - self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight) + tabsLabel = QLabel("Replace tabs with spaces") + + self.exportForm.addWidget(tabsLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight) self.exportForm.setColumnStretch(0, 1) self.exportForm.setColumnStretch(1, 0) @@ -607,6 +618,7 @@ class GuiBuildNovel(QDialog): fmtSection = self.fmtSection.text().strip() textFont = self.textFont.text() textSize = self.textSize.value() + lineHeight = self.lineHeight.value() justifyText = self.justifyText.isChecked() noStyling = self.noStyling.isChecked() incSynopsis = self.includeSynopsis.isChecked() @@ -632,6 +644,7 @@ class GuiBuildNovel(QDialog): bldObj.setFont(textFont, textSize, textFixed) bldObj.setJustify(justifyText) + bldObj.setLineHeight(lineHeight) bldObj.setSynopsis(incSynopsis) bldObj.setComments(incComments) @@ -1085,6 +1098,7 @@ class GuiBuildNovel(QDialog): noStyling = self.noStyling.isChecked() textFont = self.textFont.text() textSize = self.textSize.value() + lineHeight = self.lineHeight.value() novelFiles = self.novelFiles.isChecked() noteFiles = self.noteFiles.isChecked() ignoreFlag = self.ignoreFlag.isChecked() @@ -1111,6 +1125,7 @@ class GuiBuildNovel(QDialog): self.optState.setValue("GuiBuildNovel", "noStyling", noStyling) self.optState.setValue("GuiBuildNovel", "textFont", textFont) self.optState.setValue("GuiBuildNovel", "textSize", textSize) + self.optState.setValue("GuiBuildNovel", "lineHeight", lineHeight) self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles) self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles) self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag) From 7ea45ac3d208ac7a9af430033e948d54046feb2d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Feb 2021 10:39:25 +0100 Subject: [PATCH 074/104] Cleanup Build dialog and add line height to HTML --- nw/core/tohtml.py | 10 +++-- nw/core/toodt.py | 34 ++++++++-------- nw/gui/build.py | 99 +++++++++++++++++++++++++++-------------------- 3 files changed, 81 insertions(+), 62 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 3aa6f5fc..2434b55c 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -333,10 +333,12 @@ class ToHtml(Tokenizer): textAlign = "justify" if self.doJustify else "left" - theStyles.append("body {font-family: '%s'; font-size: %dpt}" % ( - self.textFont, self.textSize) - ) - theStyles.append("p {text-align: %s;}" % textAlign) + theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % ( + self.textFont, self.textSize + )) + theStyles.append("p {text-align: %s; line-height: %d%%;}" % ( + textAlign, round(100 * self.lineHeight) + )) theStyles.append("h1, h2 {color: rgb(66, 113, 174);}") theStyles.append("h3, h4 {color: rgb(50, 50, 50);}") theStyles.append("h1, h2, h3, h4 {page-break-after: avoid;}") diff --git a/nw/core/toodt.py b/nw/core/toodt.py index ee83673c..b9cffc3c 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -194,23 +194,25 @@ class ToOdt(Tokenizer): self._fSizeHead = f"{round(1.15 * self.textSize):d}pt" self._fSizeText = f"{self.textSize:d}pt" - self._mTopTitle = self._emToCm(self.marginTitle[0]) - self._mTopHead1 = self._emToCm(self.marginHead1[0]) - self._mTopHead2 = self._emToCm(self.marginHead2[0]) - self._mTopHead3 = self._emToCm(self.marginHead3[0]) - self._mTopHead4 = self._emToCm(self.marginHead4[0]) - self._mTopHead = self._emToCm(self.marginHead4[0]) - self._mTopText = self._emToCm(self.marginText[0]) - self._mTopMeta = self._emToCm(self.marginMeta[0]) + mScale = self.lineHeight/1.15 - self._mBotTitle = self._emToCm(self.marginTitle[1]) - self._mBotHead1 = self._emToCm(self.marginHead1[1]) - self._mBotHead2 = self._emToCm(self.marginHead2[1]) - self._mBotHead3 = self._emToCm(self.marginHead3[1]) - self._mBotHead4 = self._emToCm(self.marginHead4[1]) - self._mBotHead = self._emToCm(self.marginHead4[1]) - self._mBotText = self._emToCm(self.marginText[1]) - self._mBotMeta = self._emToCm(self.marginMeta[1]) + self._mTopTitle = self._emToCm(mScale * self.marginTitle[0]) + self._mTopHead1 = self._emToCm(mScale * self.marginHead1[0]) + self._mTopHead2 = self._emToCm(mScale * self.marginHead2[0]) + self._mTopHead3 = self._emToCm(mScale * self.marginHead3[0]) + self._mTopHead4 = self._emToCm(mScale * self.marginHead4[0]) + self._mTopHead = self._emToCm(mScale * self.marginHead4[0]) + self._mTopText = self._emToCm(mScale * self.marginText[0]) + self._mTopMeta = self._emToCm(mScale * self.marginMeta[0]) + + self._mBotTitle = self._emToCm(mScale * self.marginTitle[1]) + self._mBotHead1 = self._emToCm(mScale * self.marginHead1[1]) + self._mBotHead2 = self._emToCm(mScale * self.marginHead2[1]) + self._mBotHead3 = self._emToCm(mScale * self.marginHead3[1]) + self._mBotHead4 = self._emToCm(mScale * self.marginHead4[1]) + self._mBotHead = self._emToCm(mScale * self.marginHead4[1]) + self._mBotText = self._emToCm(mScale * self.marginText[1]) + self._mBotMeta = self._emToCm(mScale * self.marginMeta[1]) if self.colourHead: self._colHead12 = "#2a6099" diff --git a/nw/gui/build.py b/nw/gui/build.py index 14ffa0d1..38fbd727 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -93,6 +93,9 @@ class GuiBuildNovel(QDialog): self.docView = GuiBuildNovelDocView(self, self.theProject) + hS = self.theTheme.fontPixelSize + wS = 2*hS + # Title Formats # ============= @@ -191,12 +194,12 @@ class GuiBuildNovel(QDialog): self.titleForm.setColumnStretch(0, 0) self.titleForm.setColumnStretch(1, 1) - # Text Options - # ============= + # Font Options + # ============ - self.formatGroup = QGroupBox("Formatting Options", self) - self.formatForm = QGridLayout(self) - self.formatGroup.setLayout(self.formatForm) + self.fontGroup = QGroupBox("Font Options", self) + self.fontForm = QGridLayout(self) + self.fontGroup.setLayout(self.fontForm) ## Font Family self.textFont = QLineEdit() @@ -225,17 +228,7 @@ class GuiBuildNovel(QDialog): self.lineHeight.setSingleStep(0.05) self.lineHeight.setDecimals(2) self.lineHeight.setValue( - self.optState.getInt("GuiBuildNovel", "lineHeight", 1.15) - ) - - self.justifyText = QSwitch() - self.justifyText.setChecked( - self.optState.getBool("GuiBuildNovel", "justifyText", False) - ) - - self.noStyling = QSwitch() - self.noStyling.setChecked( - self.optState.getBool("GuiBuildNovel", "noStyling", False) + self.optState.getFloat("GuiBuildNovel", "lineHeight", 1.15) ) # Dummy box due to QGridView and QLineEdit expand bug @@ -248,45 +241,66 @@ class GuiBuildNovel(QDialog): justifyLabel = QLabel("Justify text") stylingLabel = QLabel("Disable styling") - self.formatForm.addWidget(fontFamilyLabel, 0, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight) - self.formatForm.addWidget(self.fontButton, 0, 2, 1, 1, Qt.AlignRight) - self.formatForm.addWidget(fontSizeLabel, 1, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.textSize, 1, 1, 1, 2, Qt.AlignRight) - self.formatForm.addWidget(lineHeightLabel, 2, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.lineHeight, 2, 1, 1, 2, Qt.AlignRight) - self.formatForm.addWidget(justifyLabel, 3, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.justifyText, 3, 1, 1, 2, Qt.AlignRight) - self.formatForm.addWidget(stylingLabel, 4, 0, 1, 1, Qt.AlignLeft) - self.formatForm.addWidget(self.noStyling, 4, 1, 1, 2, Qt.AlignRight) + self.fontForm.addWidget(fontFamilyLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.fontForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight) + self.fontForm.addWidget(self.fontButton, 0, 2, 1, 1, Qt.AlignRight) + self.fontForm.addWidget(fontSizeLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.fontForm.addWidget(self.textSize, 1, 1, 1, 2, Qt.AlignRight) + self.fontForm.addWidget(lineHeightLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.fontForm.addWidget(self.lineHeight, 2, 1, 1, 2, Qt.AlignRight) - self.formatForm.setColumnStretch(0, 0) - self.formatForm.setColumnStretch(1, 1) - self.formatForm.setColumnStretch(2, 0) + self.fontForm.setColumnStretch(0, 0) + self.fontForm.setColumnStretch(1, 1) + self.fontForm.setColumnStretch(2, 0) - # Include Switches - # ================ + # Styling Options + # =============== - self.textGroup = QGroupBox("Text Options", self) + self.styleGroup = QGroupBox("Styling Options", self) + self.styleForm = QGridLayout(self) + self.styleGroup.setLayout(self.styleForm) + + self.justifyText = QSwitch(width=wS, height=hS) + self.justifyText.setChecked( + self.optState.getBool("GuiBuildNovel", "justifyText", False) + ) + + self.noStyling = QSwitch(width=wS, height=hS) + self.noStyling.setChecked( + self.optState.getBool("GuiBuildNovel", "noStyling", False) + ) + + self.styleForm.addWidget(justifyLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.styleForm.addWidget(self.justifyText, 1, 1, 1, 2, Qt.AlignRight) + self.styleForm.addWidget(stylingLabel, 2, 0, 1, 1, Qt.AlignLeft) + self.styleForm.addWidget(self.noStyling, 2, 1, 1, 2, Qt.AlignRight) + + self.styleForm.setColumnStretch(0, 0) + self.styleForm.setColumnStretch(1, 1) + + # Include Options + # =============== + + self.textGroup = QGroupBox("Include Options", self) self.textForm = QGridLayout(self) self.textGroup.setLayout(self.textForm) - self.includeSynopsis = QSwitch() + self.includeSynopsis = QSwitch(width=wS, height=hS) self.includeSynopsis.setChecked( self.optState.getBool("GuiBuildNovel", "incSynopsis", False) ) - self.includeComments = QSwitch() + self.includeComments = QSwitch(width=wS, height=hS) self.includeComments.setChecked( self.optState.getBool("GuiBuildNovel", "incComments", False) ) - self.includeKeywords = QSwitch() + self.includeKeywords = QSwitch(width=wS, height=hS) self.includeKeywords.setChecked( self.optState.getBool("GuiBuildNovel", "incKeywords", False) ) - self.includeBody = QSwitch() + self.includeBody = QSwitch(width=wS, height=hS) self.includeBody.setChecked( self.optState.getBool("GuiBuildNovel", "incBodyText", True) ) @@ -315,7 +329,7 @@ class GuiBuildNovel(QDialog): self.fileForm = QGridLayout(self) self.fileGroup.setLayout(self.fileForm) - self.novelFiles = QSwitch() + self.novelFiles = QSwitch(width=wS, height=hS) self.novelFiles.setToolTip( "Include files with layouts 'Book', 'Page', 'Partition', " "'Chapter', 'Unnumbered', and 'Scene'." @@ -324,13 +338,13 @@ class GuiBuildNovel(QDialog): self.optState.getBool("GuiBuildNovel", "addNovel", True) ) - self.noteFiles = QSwitch() + self.noteFiles = QSwitch(width=wS, height=hS) self.noteFiles.setToolTip("Include files with layout 'Note'.") self.noteFiles.setChecked( self.optState.getBool("GuiBuildNovel", "addNotes", False) ) - self.ignoreFlag = QSwitch() + self.ignoreFlag = QSwitch(width=wS, height=hS) self.ignoreFlag.setToolTip( "Ignore the 'Include when building project' setting and include " "all files in the output." @@ -360,7 +374,7 @@ class GuiBuildNovel(QDialog): self.exportForm = QGridLayout(self) self.exportGroup.setLayout(self.exportForm) - self.replaceTabs = QSwitch() + self.replaceTabs = QSwitch(width=wS, height=hS) self.replaceTabs.setChecked( self.optState.getBool("GuiBuildNovel", "replaceTabs", False) ) @@ -458,7 +472,8 @@ class GuiBuildNovel(QDialog): # The Tool Box self.toolsBox = QVBoxLayout() self.toolsBox.addWidget(self.titleGroup) - self.toolsBox.addWidget(self.formatGroup) + self.toolsBox.addWidget(self.fontGroup) + self.toolsBox.addWidget(self.styleGroup) self.toolsBox.addWidget(self.textGroup) self.toolsBox.addWidget(self.fileGroup) self.toolsBox.addWidget(self.exportGroup) From a0fba34ff6da0135c517d9536b642b648e3c1dab Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:55:46 +0100 Subject: [PATCH 075/104] Apply line height and margins to HTML output --- nw/core/tohtml.py | 77 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 2434b55c..1198ea1b 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -331,23 +331,84 @@ class ToHtml(Tokenizer): if not self.cssStyles: return theStyles + mScale = self.lineHeight/1.15 textAlign = "justify" if self.doJustify else "left" theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % ( self.textFont, self.textSize )) - theStyles.append("p {text-align: %s; line-height: %d%%;}" % ( - textAlign, round(100 * self.lineHeight) + theStyles.append(( + "p {" + "text-align: %s; line-height: %d%%; " + "margin-top: %.2fem; margin-bottom: %.2fem;" + "}" + ) % ( + textAlign, + round(100 * self.lineHeight), + mScale * self.marginText[0], + mScale * self.marginText[1], )) - theStyles.append("h1, h2 {color: rgb(66, 113, 174);}") - theStyles.append("h3, h4 {color: rgb(50, 50, 50);}") - theStyles.append("h1, h2, h3, h4 {page-break-after: avoid;}") + theStyles.append(( + "h1 {" + "color: rgb(66, 113, 174); " + "page-break-after: avoid; " + "margin-top: %.2fem; " + "margin-bottom: %.2fem;" + "}" + ) % ( + mScale * self.marginHead1[0], mScale * self.marginHead1[1] + )) + theStyles.append(( + "h2 {" + "color: rgb(66, 113, 174); " + "page-break-after: avoid; " + "margin-top: %.2fem; " + "margin-bottom: %.2fem;" + "}" + ) % ( + mScale * self.marginHead2[0], mScale * self.marginHead2[1] + )) + theStyles.append(( + "h3 {" + "color: rgb(50, 50, 50); " + "page-break-after: avoid; " + "margin-top: %.2fem; " + "margin-bottom: %.2fem;" + "}" + ) % ( + mScale * self.marginHead3[0], mScale * self.marginHead3[1] + )) + theStyles.append(( + "h4 {" + "color: rgb(50, 50, 50); " + "page-break-after: avoid; " + "margin-top: %.2fem; " + "margin-bottom: %.2fem;" + "}" + ) % ( + mScale * self.marginHead4[0], mScale * self.marginHead4[1] + )) + theStyles.append(( + ".title {" + "font-size: 2.5em; " + "margin-top: %.2fem; " + "margin-bottom: %.2fem;" + "}" + ) % ( + mScale * self.marginTitle[0], mScale * self.marginTitle[1] + )) + theStyles.append(( + ".sep, .skip {" + "text-align: center; " + "margin-top: %.2fem; " + "margin-bottom: %.2fem;}" + ) % ( + mScale, mScale + )) + theStyles.append("a {color: rgb(66, 113, 174);}") - theStyles.append(".title {font-size: 2.5em;}") theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}") theStyles.append(".break {text-align: left;}") - theStyles.append(".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}") - theStyles.append(".skip {margin-top: 1em; margin-bottom: 1em;}") theStyles.append(".synopsis {font-style: italic;}") theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}") From f67a50c064d9db68b996a9fec93f907014955a9d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:56:52 +0100 Subject: [PATCH 076/104] Change the way Unicode is handled in HTML, and clean up the class a bit --- nw/constants/__init__.py | 4 +- nw/constants/constants.py | 61 ++++++++++++++++++++++++++++- nw/core/tohtml.py | 80 +++++++++++++-------------------------- nw/core/tokenizer.py | 13 +++++-- nw/gui/docviewer.py | 2 +- 5 files changed, 98 insertions(+), 62 deletions(-) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index b58b56c3..1a6748a1 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from nw.constants.iso import isoLanguage, isoCountry from nw.constants.constants import ( - nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode + nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, + nwUnicode, nwHtmlUnicode ) from nw.constants.enum import ( nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline, @@ -19,6 +20,7 @@ __all__ = [ "nwLabels", "nwQuotes", "nwUnicode", + "nwHtmlUnicode", "nwAlert", "nwDocAction", "nwItemClass", diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 5e4581b9..0909b1f2 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -265,7 +265,7 @@ class nwUnicode: U_LCQUO = "\u300c" # Left corner bracket U_RCQUO = "\u300d" # Right corner bracket U_LWCQUO = "\u300e" # Left white corner bracket - U_RECQUO = "\u300f" # Right white corner bracket + U_RWCQUO = "\u300f" # Right white corner bracket ## Punctuation U_FGDASH = "\u2012" # Figure dash @@ -331,7 +331,7 @@ class nwUnicode: H_LCQUO = "「" H_RCQUO = "」" H_LWCQUO = "『" - H_LWCQUO = "『" + H_RWCQUO = "』" ## Punctuation H_FGDASH = "‒" @@ -374,3 +374,60 @@ class nwUnicode: H_LTRIS = "◂" # END Class nwUnicode + +class nwHtmlUnicode(): + + U_TO_H = { + ## Quotes + nwUnicode.U_QUOT : nwUnicode.H_QUOT, + nwUnicode.U_APOS : nwUnicode.H_APOS, + nwUnicode.U_LAQUO : nwUnicode.H_LAQUO, + nwUnicode.U_RAQUO : nwUnicode.H_RAQUO, + nwUnicode.U_LSQUO : nwUnicode.H_LSQUO, + nwUnicode.U_RSQUO : nwUnicode.H_RSQUO, + nwUnicode.U_SBQUO : nwUnicode.H_SBQUO, + nwUnicode.U_SUQUO : nwUnicode.H_SUQUO, + nwUnicode.U_LDQUO : nwUnicode.H_LDQUO, + nwUnicode.U_RDQUO : nwUnicode.H_RDQUO, + nwUnicode.U_BDQUO : nwUnicode.H_BDQUO, + nwUnicode.U_UDQUO : nwUnicode.H_UDQUO, + nwUnicode.U_LSAQUO : nwUnicode.H_LSAQUO, + nwUnicode.U_RSAQUO : nwUnicode.H_RSAQUO, + nwUnicode.U_BDRQUO : nwUnicode.H_BDRQUO, + nwUnicode.U_LCQUO : nwUnicode.H_LCQUO, + nwUnicode.U_RCQUO : nwUnicode.H_RCQUO, + nwUnicode.U_LWCQUO : nwUnicode.H_LWCQUO, + nwUnicode.U_RWCQUO : nwUnicode.H_RWCQUO, + + ## Punctuation + nwUnicode.U_FGDASH : nwUnicode.H_FGDASH, + nwUnicode.U_ENDASH : nwUnicode.H_ENDASH, + nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, + nwUnicode.U_HBAR : nwUnicode.H_HBAR, + nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, + nwUnicode.U_MAPOSS : nwUnicode.H_MAPOSS, + nwUnicode.U_PRIME : nwUnicode.H_PRIME, + nwUnicode.U_DPRIME : nwUnicode.H_DPRIME, + + ## Spaces + nwUnicode.U_NBSP : nwUnicode.H_NBSP, + nwUnicode.U_THSP : nwUnicode.H_THSP, + nwUnicode.U_THNBSP : nwUnicode.H_THNBSP, + nwUnicode.U_ENSP : nwUnicode.H_ENSP, + nwUnicode.U_EMSP : nwUnicode.H_EMSP, + + ## Symbols + nwUnicode.U_CHECK : nwUnicode.H_CHECK, + nwUnicode.U_CROSS : nwUnicode.H_CROSS, + nwUnicode.U_BULL : nwUnicode.H_BULL, + nwUnicode.U_TRBULL : nwUnicode.H_TRBULL, + nwUnicode.U_HYBULL : nwUnicode.H_HYBULL, + nwUnicode.U_FLOWER : nwUnicode.H_FLOWER, + nwUnicode.U_PERMIL : nwUnicode.H_PERMIL, + nwUnicode.U_DEGREE : nwUnicode.H_DEGREE, + nwUnicode.U_MINUS : nwUnicode.H_MINUS, + nwUnicode.U_TIMES : nwUnicode.H_TIMES, + nwUnicode.U_DIVIDE : nwUnicode.H_DIVIDE, + } + +# END Class nwHtmlUnicode diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 1198ea1b..67ebcc0f 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -25,10 +25,9 @@ along with this program. If not, see . """ import logging -import re from nw.core.tokenizer import Tokenizer -from nw.constants import nwUnicode, nwLabels, nwKeyWords +from nw.constants import nwLabels, nwKeyWords, nwHtmlUnicode logger = logging.getLogger(__name__) @@ -41,27 +40,13 @@ class ToHtml(Tokenizer): def __init__(self, theProject, theParent): Tokenizer.__init__(self, theProject, theParent) - self.genMode = self.M_EXPORT + self.genMode = self.M_EXPORT self.cssStyles = True + self.fullHTML = [] - self.repDict = { - "<" : "<", - ">" : ">", - "&" : "&", - nwUnicode.U_ENDASH : nwUnicode.H_ENDASH, - nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, - nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, - nwUnicode.U_NBSP : nwUnicode.H_NBSP, - nwUnicode.U_THSP : nwUnicode.H_THSP, - nwUnicode.U_THNBSP : nwUnicode.H_THNBSP, - nwUnicode.U_MAPOSS : nwUnicode.H_RSQUO, - } - self.revDict = {} - self.reReplace = [] - self.reReverse = [] - self._buildRegEx() - - self.fullHTML = [] + # Internals + self._trMap = {} + self.setReplaceUnicode(False) return @@ -88,6 +73,23 @@ class ToHtml(Tokenizer): self.cssStyles = cssStyles return + def setReplaceUnicode(self, doReplace): + """Set the translation map to either minimal or full unicode to + html entities replacement. + """ + # Control characters must always be replaced + self._trMap = str.maketrans({ + "<" : "<", + ">" : ">", + "&" : "&", + }) + + if doReplace: + # Extend to all relevant Unicode characters + self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H)) + + return + ## # Class Methods ## @@ -97,30 +99,12 @@ class ToHtml(Tokenizer): """ return sum([len(x) for x in self.fullHTML]) - def doAutoReplace(self): + def doPreProcessing(self): """Extend the auto-replace to also properly encode some unicode characters into their respective HTML entities. """ - Tokenizer.doAutoReplace(self) - self.theText = self.reReplace.sub( - lambda x: self.repDict[x.group(0)], self.theText - ) - return - - def doPostProcessing(self): - """Reverse the html entities replacement on the markdown text. - Otherwise, all the &something; bits will also be in there. - """ - Tokenizer.doPostProcessing(self) - if self.genMode == self.M_PREVIEW: - # Doesn't matter for preview as we don't use the markdown - return - - if self.keepMarkdown: - self.theMarkdown[-1] = self.reReverse.sub( - lambda x: self.revDict[x.group(0)], self.theMarkdown[-1] - ) - + Tokenizer.doPreProcessing(self) + self.theText = self.theText.translate(self._trMap) return def doConvert(self): @@ -466,16 +450,4 @@ class ToHtml(Tokenizer): return retText - def _buildRegEx(self): - """Build the regular expressions - """ - self.revDict = dict(map(reversed, self.repDict.items())) - self.reReplace = re.compile( - "|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL - ) - self.reReverse = re.compile( - "|".join([re.escape(k) for k in self.revDict.keys()]), flags=re.DOTALL - ) - return - # END Class ToHtml diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 96b4be2d..3e0474ae 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import QRegularExpression from nw.core.document import NWDoc from nw.core.tools import numberToWord, numberToRoman -from nw.constants import nwConst, nwItemLayout, nwItemType, nwRegEx +from nw.constants import nwConst, nwUnicode, nwItemLayout, nwItemType, nwRegEx logger = logging.getLogger(__name__) @@ -86,7 +86,7 @@ class Tokenizer(): self.theResult = "" # The result of the last document self.keepMarkdown = False # Whether to keep the markdown text - self.theMarkdown = [] # The result novelWriter markdown of all documents + self.theMarkdown = [] # The result novelWriter markdown of all documents # User Settings self.textFont = "Serif" # Output text font @@ -297,9 +297,10 @@ class Tokenizer(): return True - def doAutoReplace(self): - """Run through the user's auto-replace dictionary. + def doPreProcessing(self): + """Reun trough the various replace doctionaries. """ + # Process the user's auto-replace dictionary if len(self.theProject.autoReplace) > 0: repDict = {} for aKey, aVal in self.theProject.autoReplace.items(): @@ -307,6 +308,10 @@ class Tokenizer(): xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText) + # Process the character translation map + trDict = {nwUnicode.U_MAPOSS: nwUnicode.U_RSQUO} + self.theText = self.theText.translate(str.maketrans(trDict)) + return def doPostProcessing(self): diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 24d53019..ddc0d876 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -176,7 +176,7 @@ class GuiDocViewer(QTextBrowser): # See issue #298 try: aDoc.setText(tHandle) - aDoc.doAutoReplace() + aDoc.doPreProcessing() aDoc.tokenizeText() aDoc.doConvert() aDoc.doPostProcessing() From ec189a4867b1bab9d04b1b19ff286032c1f9a35a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:57:15 +0100 Subject: [PATCH 077/104] Clean up the Build dialog and connect new options --- nw/core/options.py | 1 + nw/gui/build.py | 113 +++++++++++++++++++++++++-------------------- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index c47973e8..52a30af5 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -80,6 +80,7 @@ class OptionState(): "incKeywords", "incBodyText", "replaceTabs", + "replaceUCode", }, "GuiOutline": { "headerOrder", diff --git a/nw/gui/build.py b/nw/gui/build.py index 38fbd727..2687ccde 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -55,15 +55,17 @@ logger = logging.getLogger(__name__) class GuiBuildNovel(QDialog): - FMT_ODT = 1 - FMT_FODT = 2 - FMT_PDF = 3 - FMT_HTM = 4 - FMT_MD = 5 - FMT_GH = 6 - FMT_NWD = 7 - FMT_JSON_H = 8 - FMT_JSON_M = 9 + FMT_PDF = 1 # Print to PDF + + FMT_ODT = 2 # Open Document file + FMT_FODT = 3 # Flat Open Document file + FMT_HTM = 4 # HTML5 + FMT_NWD = 5 # nW Markdown + FMT_MD = 6 # Standard Markdown + FMT_GH = 7 # GitHub Markdown + + FMT_JSON_H = 8 # HTML5 wrapped in JSON + FMT_JSON_M = 9 # nW Markdown wrapped in JSON def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -379,10 +381,18 @@ class GuiBuildNovel(QDialog): self.optState.getBool("GuiBuildNovel", "replaceTabs", False) ) - tabsLabel = QLabel("Replace tabs with spaces") + self.replaceUCode = QSwitch(width=wS, height=hS) + self.replaceUCode.setChecked( + self.optState.getBool("GuiBuildNovel", "replaceUCode", False) + ) - self.exportForm.addWidget(tabsLabel, 0, 0, 1, 1, Qt.AlignLeft) - self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight) + tabsLabel = QLabel("Replace tabs with spaces") + uCodeLabel = QLabel("Replace Unicode in HTML") + + self.exportForm.addWidget(tabsLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight) + self.exportForm.addWidget(uCodeLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.exportForm.addWidget(self.replaceUCode, 1, 1, 1, 1, Qt.AlignRight) self.exportForm.setColumnStretch(0, 1) self.exportForm.setColumnStretch(1, 0) @@ -643,6 +653,7 @@ class GuiBuildNovel(QDialog): noteFiles = self.noteFiles.isChecked() ignoreFlag = self.ignoreFlag.isChecked() includeBody = self.includeBody.isChecked() + replaceUCode = self.replaceUCode.isChecked() # Get font information fontInfo = QFontInfo(QFont(textFont, textSize)) @@ -668,6 +679,7 @@ class GuiBuildNovel(QDialog): if isHtml: bldObj.setStyles(not noStyling) + bldObj.setReplaceUnicode(replaceUCode) if isOdt: bldObj.setColourHeaders(not noStyling) @@ -696,7 +708,7 @@ class GuiBuildNovel(QDialog): elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): bldObj.setText(tItem.itemHandle) - bldObj.doAutoReplace() + bldObj.doPreProcessing() bldObj.tokenizeText() bldObj.doHeaders() if doConvert: @@ -1107,48 +1119,47 @@ class GuiBuildNovel(QDialog): "section" : self.fmtSection.text().strip(), }) - winWidth = self.mainConf.rpxInt(self.width()) - winHeight = self.mainConf.rpxInt(self.height()) - justifyText = self.justifyText.isChecked() - noStyling = self.noStyling.isChecked() - textFont = self.textFont.text() - textSize = self.textSize.value() - lineHeight = self.lineHeight.value() - novelFiles = self.novelFiles.isChecked() - noteFiles = self.noteFiles.isChecked() - ignoreFlag = self.ignoreFlag.isChecked() - incSynopsis = self.includeSynopsis.isChecked() - incComments = self.includeComments.isChecked() - incKeywords = self.includeKeywords.isChecked() - incBodyText = self.includeBody.isChecked() - replaceTabs = self.replaceTabs.isChecked() + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) + justifyText = self.justifyText.isChecked() + noStyling = self.noStyling.isChecked() + textFont = self.textFont.text() + textSize = self.textSize.value() + lineHeight = self.lineHeight.value() + novelFiles = self.novelFiles.isChecked() + noteFiles = self.noteFiles.isChecked() + ignoreFlag = self.ignoreFlag.isChecked() + incSynopsis = self.includeSynopsis.isChecked() + incComments = self.includeComments.isChecked() + incKeywords = self.includeKeywords.isChecked() + incBodyText = self.includeBody.isChecked() + replaceTabs = self.replaceTabs.isChecked() + replaceUCode = self.replaceUCode.isChecked() mainSplit = self.mainSplit.sizes() - if len(mainSplit) == 2: - boxWidth = self.mainConf.rpxInt(mainSplit[0]) - docWidth = self.mainConf.rpxInt(mainSplit[1]) - else: - boxWidth = 100 - docWidth = 100 + boxWidth = self.mainConf.rpxInt(mainSplit[0]) + docWidth = self.mainConf.rpxInt(mainSplit[1]) # GUI Settings - self.optState.setValue("GuiBuildNovel", "winWidth", winWidth) - self.optState.setValue("GuiBuildNovel", "winHeight", winHeight) - self.optState.setValue("GuiBuildNovel", "boxWidth", boxWidth) - self.optState.setValue("GuiBuildNovel", "docWidth", docWidth) - self.optState.setValue("GuiBuildNovel", "justifyText", justifyText) - self.optState.setValue("GuiBuildNovel", "noStyling", noStyling) - self.optState.setValue("GuiBuildNovel", "textFont", textFont) - self.optState.setValue("GuiBuildNovel", "textSize", textSize) - self.optState.setValue("GuiBuildNovel", "lineHeight", lineHeight) - self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles) - self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles) - self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag) - self.optState.setValue("GuiBuildNovel", "incSynopsis", incSynopsis) - self.optState.setValue("GuiBuildNovel", "incComments", incComments) - self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords) - self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText) - self.optState.setValue("GuiBuildNovel", "replaceTabs", replaceTabs) + self.optState.setValue("GuiBuildNovel", "winWidth", winWidth) + self.optState.setValue("GuiBuildNovel", "winHeight", winHeight) + self.optState.setValue("GuiBuildNovel", "boxWidth", boxWidth) + self.optState.setValue("GuiBuildNovel", "docWidth", docWidth) + self.optState.setValue("GuiBuildNovel", "justifyText", justifyText) + self.optState.setValue("GuiBuildNovel", "noStyling", noStyling) + self.optState.setValue("GuiBuildNovel", "textFont", textFont) + self.optState.setValue("GuiBuildNovel", "textSize", textSize) + self.optState.setValue("GuiBuildNovel", "lineHeight", lineHeight) + self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles) + self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles) + self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag) + self.optState.setValue("GuiBuildNovel", "incSynopsis", incSynopsis) + self.optState.setValue("GuiBuildNovel", "incComments", incComments) + self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords) + self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText) + self.optState.setValue("GuiBuildNovel", "replaceTabs", replaceTabs) + self.optState.setValue("GuiBuildNovel", "replaceUCode", replaceUCode) + self.optState.saveSettings() return From 672ea6d6f2f9be81302f881714794b8ede7806f0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:57:32 +0100 Subject: [PATCH 078/104] Fix and update tests --- .../guiBuild_Tool_Step1_Lorem_Ipsum.htm | 20 +++++----- .../guiBuild_Tool_Step2_Lorem_Ipsum.htm | 32 +++++++-------- .../guiBuild_Tool_Step3_Lorem_Ipsum.htm | 32 +++++++-------- .../guiBuild_Tool_Step4H_Lorem_Ipsum.json | 18 ++++----- .../guiBuild_Tool_Step4_Lorem_Ipsum.htm | 16 ++++---- tests/test_core/test_core_tohtml.py | 39 +++++++++++-------- tests/test_core/test_core_tokenizer.py | 4 +- tests/test_gui/test_gui_build.py | 2 + 8 files changed, 85 insertions(+), 78 deletions(-) diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm index 413f48df..5fa77a9b 100644 --- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm +++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm @@ -5,17 +5,17 @@ Lorem Ipsum