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 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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 10/20] 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 18/20] 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 19/20] 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 20/20] 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.

-