From c9eebafb2dd2b2db366a4cd1cbd336113b03dddf Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 26 Jan 2021 16:25:14 +0100
Subject: [PATCH 001/104] Add core structure for ODT class
---
nw/core/__init__.py | 2 ++
nw/core/toodt.py | 55 ++++++++++++++++++++++++++++++
nw/gui/projdetails.py | 2 +-
tests/test_core/test_core_toodt.py | 35 +++++++++++++++++++
4 files changed, 93 insertions(+), 1 deletion(-)
create mode 100644 nw/core/toodt.py
create mode 100644 tests/test_core/test_core_toodt.py
diff --git a/nw/core/__init__.py b/nw/core/__init__.py
index f5140ebc..fc90379b 100644
--- a/nw/core/__init__.py
+++ b/nw/core/__init__.py
@@ -5,6 +5,7 @@ from nw.core.index import NWIndex
from nw.core.project import NWProject
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
from nw.core.tohtml import ToHtml
+from nw.core.toodt import ToOdt
from nw.core.tools import countWords, numberToRoman, numberToWord
__all__ = [
@@ -18,4 +19,5 @@ __all__ = [
"NWSpellEnchant",
"NWSpellSimple",
"ToHtml",
+ "ToOdt",
]
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
new file mode 100644
index 00000000..5bad6615
--- /dev/null
+++ b/nw/core/toodt.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+novelWriter – ODT Text Converter
+================================
+Extends the Tokenizer class to generate ODT and FODT files
+
+File History:
+Created: 2021-01-26 [1.1rc1]
+
+This file is a part of novelWriter
+Copyright 2018–2021, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+import logging
+
+from nw.core.tokenizer import Tokenizer
+
+logger = logging.getLogger(__name__)
+
+class ToOdt(Tokenizer):
+
+ def __init__(self, theProject, theParent):
+ Tokenizer.__init__(self, theProject, theParent)
+
+ return
+
+ ##
+ # Class Methods
+ ##
+
+ def doConvert(self):
+ """Convert the list of text tokens into a HTML document saved
+ to theResult.
+ """
+ self.theResult = ""
+
+ for tType, tLine, tText, tFormat, tStyle in self.theTokens:
+ continue
+
+ return
+
+# END Class ToOdt
diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py
index 0fabaae9..d37cebee 100644
--- a/nw/gui/projdetails.py
+++ b/nw/gui/projdetails.py
@@ -5,7 +5,7 @@ novelWriter – GUI Project Details
Class holding the project details dialog
File History:
-Created: 2021-01-03 [1.0a0]
+Created: 2021-01-03 [1.1a0]
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
new file mode 100644
index 00000000..c9fe5b10
--- /dev/null
+++ b/tests/test_core/test_core_toodt.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+"""
+novelWriter – ToOdt Class Tester
+=================================
+
+This file is a part of novelWriter
+Copyright 2018–2021, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+import pytest
+
+from nw.core import NWProject, NWIndex, ToOdt
+
+@pytest.mark.core
+def testCoreToOdt_Convert(dummyGUI):
+ """Test the converter of the ToHtml class.
+ """
+ theProject = NWProject(dummyGUI)
+ dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
+ theDoc = ToOdt(theProject, dummyGUI)
+
+# END Test testCoreToOdt_Convert
From e3c37f003be2eb6466a1a192e121de415ade20b6 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Tue, 26 Jan 2021 20:22:32 +0100
Subject: [PATCH 002/104] Add main ODT style XML
---
nw/core/toodt.py | 303 ++++++++++++++++++++++++++++-
tests/test_core/test_core_toodt.py | 18 +-
2 files changed, 319 insertions(+), 2 deletions(-)
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index 5bad6615..09fbb91f 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -24,23 +24,87 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import nw
import logging
+import os
+
+from lxml import etree
from nw.core.tokenizer import Tokenizer
logger = logging.getLogger(__name__)
+XML_NS = {
+ "office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
+ "style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+ "text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
+ "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+ "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
+}
+
class ToOdt(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
+ self.mainConf = nw.CONFIG
+
+ self._xRoot = None
+ self._xStyl = None
+ self._xText = None
+
+ self._dLanguage = "en"
+ self._dCountry = "GB"
+ self._dFontFace = "Liberation Serif"
+ self._dFontSize = 12
+
+ return
+
+ ##
+ # Setters
+ ##
+
+ def setLanguage(self, theLang):
+ """Set language for the document.
+ """
+ if theLang is None:
+ return False
+
+ langBits = theLang.split("_")
+ self._dLanguage = langBits[0]
+ if len(langBits) > 1:
+ self._dCountry = langBits[1]
+
+ return True
+
+ def setFont(self, fontFace, fontSize):
+ """Set font and font size.
+ """
+ self._dFontFace = fontFace
+ self._dFontSize = fontSize
return
##
# Class Methods
##
+ def initDocument(self):
+ """Initialises a new open document XML tree.
+ """
+ rAttr = {
+ _mkTag("office", "version") : "1.3",
+ _mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text",
+ }
+ self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS)
+ self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles"))
+ self._xText = etree.SubElement(self._xRoot, _mkTag("office", "text"))
+
+ # Add Styles
+ self._styleParagraph()
+ self._styleHeaders()
+
+ return
+
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
@@ -48,8 +112,245 @@ class ToOdt(Tokenizer):
self.theResult = ""
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
- continue
+
+ # Process Text Type
+ if tType == self.T_EMPTY:
+ continue
+
+ elif tType == self.T_TITLE:
+ continue
+
+ return
+
+ def closeDocument(self):
+ """Return the serialised XML document
+ """
+ self.theResult = etree.tostring(
+ self._xRoot,
+ pretty_print = True,
+ encoding = "utf-8",
+ xml_declaration = True
+ )
+
+ cacheFile = os.path.join(os.path.expanduser("~"), "Temp", "odtGen.fodt")
+ with open(cacheFile, mode="wb") as outFile:
+ outFile.write(self.theResult)
+
+ return
+
+ ##
+ # Style Elements
+ ##
+
+ def _styleParagraph(self):
+ """Set the paragraph styles.
+ """
+ # Add Default Paragraph Style
+ # ===========================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "line-break")] = "strict"
+ theAttr[_mkTag("style", "tab-stop-distance")] = "1.251cm"
+ theAttr[_mkTag("style", "writing-mode")] = "page"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "font-name")] = self._dFontFace
+ theAttr[_mkTag("fo", "font-size")] = "%dpt" % self._dFontSize
+ theAttr[_mkTag("fo", "language")] = self._dLanguage
+ theAttr[_mkTag("fo", "country")] = self._dCountry
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Paragraph Style
+ # ===================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Standard"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "class")] = "text"
+ etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ # Add Text Body Style
+ # ===================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Text_body"
+ theAttr[_mkTag("style", "display-name")] = "Text Body"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Standard"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = "0cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.247cm"
+ theAttr[_mkTag("style", "contextual-spacing")] = "false"
+ theAttr[_mkTag("fo", "line-height")] = "115%"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ return
+
+ def _styleHeaders(self):
+ """Set the header styles.
+ """
+ # Add Default Heading Style
+ # =========================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Heading"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Standard"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("style", "contextual-spacing")] = "false"
+ theAttr[_mkTag("fo", "keep-with-next")] = "always"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "font-name")] = self._dFontFace
+ theAttr[_mkTag("fo", "font-family")] = "'%s'" % self._dFontFace
+ theAttr[_mkTag("style", "font-pitch")] = "variable"
+ theAttr[_mkTag("fo", "font-size")] = "14pt"
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Title Style
+ # ===============
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Title"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Heading"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "class")] = "chapter"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "text-align")] = "center"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "font-size")] = "28pt"
+ theAttr[_mkTag("fo", "font-weight")] = "bold"
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Heading 1 Style
+ # ===================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Heading_1"
+ theAttr[_mkTag("style", "display-name")] = "Heading 1"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Heading"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "default-outline-level")] = "1"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("style", "contextual-spacing")] = "false"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "font-size")] = "130%"
+ theAttr[_mkTag("fo", "font-weight")] = "bold"
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Heading 2 Style
+ # ===================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Heading_2"
+ theAttr[_mkTag("style", "display-name")] = "Heading 2"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Heading"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "default-outline-level")] = "2"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = "0.353cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("style", "contextual-spacing")] = "false"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "font-size")] = "120%"
+ theAttr[_mkTag("fo", "font-weight")] = "bold"
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Heading 3 Style
+ # ===================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Heading_3"
+ theAttr[_mkTag("style", "display-name")] = "Heading 3"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Heading"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "default-outline-level")] = "3"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = "0.247cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("style", "contextual-spacing")] = "false"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "font-size")] = "110%"
+ theAttr[_mkTag("fo", "font-weight")] = "bold"
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Heading 4 Style
+ # ===================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Heading_4"
+ theAttr[_mkTag("style", "display-name")] = "Heading 4"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Heading"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "default-outline-level")] = "4"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = "0.247cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("style", "contextual-spacing")] = "false"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "font-size")] = "100%"
+ theAttr[_mkTag("fo", "font-weight")] = "bold"
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
return
# END Class ToOdt
+
+# =========================================================================== #
+# Local Functions
+# =========================================================================== #
+
+def _mkTag(nsName, tagName):
+ """Assemble namespace and tag name.
+ """
+ theNS = XML_NS.get(nsName, "")
+ if theNS:
+ return "{%s}%s" % (theNS, tagName)
+ logger.warning("Missing xml namespace '%s'" % nsName)
+ return tagName
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index c9fe5b10..6159929a 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -20,16 +20,32 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import nw
import pytest
from nw.core import NWProject, NWIndex, ToOdt
@pytest.mark.core
-def testCoreToOdt_Convert(dummyGUI):
+def testCoreToOdt_Convert(tmpConf, dummyGUI):
"""Test the converter of the ToHtml class.
"""
+ nw.CONFIG = tmpConf
+
theProject = NWProject(dummyGUI)
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
theDoc = ToOdt(theProject, dummyGUI)
+ # Export Mode
+ # ===========
+
+ theDoc.isNovel = True
+
+ # Header 1
+ theDoc.theText = "# Title\n"
+ theDoc.tokenizeText()
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert theDoc.theResult == ""
+
# END Test testCoreToOdt_Convert
From da2e40db4dca39e4db45c6f16bae7abeb86e431b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 01:12:09 +0100
Subject: [PATCH 003/104] Add ODT auto-style logic
---
nw/core/toodt.py | 478 +++++++++++++++++++++--------
tests/test_core/test_core_toodt.py | 35 ++-
2 files changed, 383 insertions(+), 130 deletions(-)
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index 09fbb91f..c89c065d 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -29,6 +29,7 @@ import logging
import os
from lxml import etree
+from hashlib import sha256
from nw.core.tokenizer import Tokenizer
@@ -39,7 +40,6 @@ XML_NS = {
"style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
- "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
}
class ToOdt(Tokenizer):
@@ -51,7 +51,13 @@ class ToOdt(Tokenizer):
self._xRoot = None
self._xStyl = None
+ self._xBody = None
self._xText = None
+ self._xAuto = None
+
+ self._mainPara = {}
+ self._autoPara = {}
+ self._autoText = {}
self._dLanguage = "en"
self._dCountry = "GB"
@@ -97,11 +103,13 @@ class ToOdt(Tokenizer):
}
self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS)
self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles"))
- self._xText = etree.SubElement(self._xRoot, _mkTag("office", "text"))
+ self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles"))
+ self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body"))
+ self._xText = etree.SubElement(self._xBody, _mkTag("office", "text"))
# Add Styles
- self._styleParagraph()
- self._styleHeaders()
+ self._defaultStyles()
+ self._useableStyles()
return
@@ -113,18 +121,60 @@ class ToOdt(Tokenizer):
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
+ # Styles
+ oStyle = ODTParagraphStyle()
+ if tStyle is not None:
+ if tStyle & self.A_LEFT:
+ oStyle.setTextAlign("left")
+ if tStyle & self.A_RIGHT:
+ oStyle.setTextAlign("right")
+ if tStyle & self.A_CENTRE:
+ oStyle.setTextAlign("center")
+ if tStyle & self.A_JUSTIFY:
+ oStyle.setTextAlign("justify")
+ if tStyle & self.A_PBB:
+ oStyle.setBreakBefore("page")
+ if tStyle & self.A_PBA:
+ oStyle.setBreakAfter("page")
+
# Process Text Type
if tType == self.T_EMPTY:
continue
elif tType == self.T_TITLE:
- continue
+ tHead = tText.replace(r"\\", "
")
+ self._addTextPar("Title", oStyle, tHead)
+
+ elif tType == self.T_HEAD1:
+ tHead = tText.replace(r"\\", "
")
+ self._addTextPar("Heading_1", oStyle, tHead, oLevel="1")
+
+ elif tType == self.T_HEAD2:
+ tHead = tText.replace(r"\\", "
")
+ self._addTextPar("Heading_2", oStyle, tHead, oLevel="2")
+
+ elif tType == self.T_HEAD3:
+ tHead = tText.replace(r"\\", "
")
+ self._addTextPar("Heading_3", oStyle, tHead, oLevel="3")
+
+ elif tType == self.T_HEAD4:
+ tHead = tText.replace(r"\\", "
")
+ self._addTextPar("Heading_4", oStyle, tHead, oLevel="4")
+
+ elif tType == self.T_SEP:
+ self._addTextPar("Text_Body", oStyle, tText)
+
+ elif tType == self.T_SKIP:
+ self._addTextPar("Text_Body", oStyle, "")
return
def closeDocument(self):
"""Return the serialised XML document
"""
+ for styleName, styleObj in self._autoPara.values():
+ styleObj.packXML(self._xAuto, styleName)
+
self.theResult = etree.tostring(
self._xRoot,
pretty_print = True,
@@ -138,15 +188,51 @@ class ToOdt(Tokenizer):
return
+ ##
+ # Internal Functions
+ ##
+
+ def _addTextPar(self, styleName, oStyle, theText, oLevel=None):
+ """Add a text paragraph to the text XML element.
+ """
+ tAttr = {}
+ tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle)
+ if oLevel is not None:
+ tAttr[_mkTag("text", "outline-level")] = oLevel
+ xElem = etree.SubElement(self._xText, _mkTag("text", "p"), attrib=tAttr)
+ xElem.text = theText
+ return
+
+ def _paraStyle(self, parName, oStyle):
+ """Return a name for a style object.
+ """
+ refStyle = self._mainPara.get(parName, None)
+ if refStyle is None:
+ logger.error("Unknown paragraph style '%s'" % parName)
+ return "Standard"
+
+ if not refStyle.checkNew(oStyle):
+ return parName
+
+ oStyle.setParentStyleName(parName)
+ theID = oStyle.getID()
+ if theID in self._autoPara:
+ return self._autoPara[theID][0]
+
+ newName = "P%d" % (len(self._autoPara) + 1)
+ self._autoPara[theID] = (newName, oStyle)
+
+ return newName
+
##
# Style Elements
##
- def _styleParagraph(self):
- """Set the paragraph styles.
+ def _defaultStyles(self):
+ """Set the default styles.
"""
- # Add Default Paragraph Style
- # ===========================
+ # Add Paragraph Family Style
+ # ==========================
theAttr = {}
theAttr[_mkTag("style", "family")] = "paragraph"
@@ -165,8 +251,8 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("fo", "country")] = self._dCountry
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
- # Add Paragraph Style
- # ===================
+ # Add Standard Paragraph Style
+ # ============================
theAttr = {}
theAttr[_mkTag("style", "name")] = "Standard"
@@ -174,29 +260,6 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("style", "class")] = "text"
etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
- # Add Text Body Style
- # ===================
-
- theAttr = {}
- theAttr[_mkTag("style", "name")] = "Text_body"
- theAttr[_mkTag("style", "display-name")] = "Text Body"
- theAttr[_mkTag("style", "family")] = "paragraph"
- theAttr[_mkTag("style", "parent-style-name")] = "Standard"
- theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
-
- theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.247cm"
- theAttr[_mkTag("style", "contextual-spacing")] = "false"
- theAttr[_mkTag("fo", "line-height")] = "115%"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
-
- return
-
- def _styleHeaders(self):
- """Set the header styles.
- """
# Add Default Heading Style
# =========================
@@ -204,14 +267,13 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("style", "name")] = "Heading"
theAttr[_mkTag("style", "family")] = "paragraph"
theAttr[_mkTag("style", "parent-style-name")] = "Standard"
- theAttr[_mkTag("style", "next-style-name")] = "Text_body"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_Body"
theAttr[_mkTag("style", "class")] = "text"
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
- theAttr[_mkTag("style", "contextual-spacing")] = "false"
theAttr[_mkTag("fo", "keep-with-next")] = "always"
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
@@ -222,129 +284,287 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("fo", "font-size")] = "14pt"
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ return
+
+ def _useableStyles(self):
+ """Set the usable styles.
+ """
+ # Add Text Body Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Text Body")
+ oStyle.setParentStyleName("Standard")
+ oStyle.setClass("text")
+ oStyle.setMarginTop("0cm")
+ oStyle.setMarginBottom("0.247cm")
+ oStyle.setLineHeight("115%")
+ oStyle.packXML(self._xStyl, "Text_Body")
+
+ self._mainPara["Text_Body"] = oStyle
+
# Add Title Style
# ===============
- theAttr = {}
- theAttr[_mkTag("style", "name")] = "Title"
- theAttr[_mkTag("style", "family")] = "paragraph"
- theAttr[_mkTag("style", "parent-style-name")] = "Heading"
- theAttr[_mkTag("style", "next-style-name")] = "Text_body"
- theAttr[_mkTag("style", "class")] = "chapter"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Title")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setClass("chapter")
+ oStyle.setTextAlign("center")
+ oStyle.setFontSize("28pt")
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Title")
- theAttr = {}
- theAttr[_mkTag("fo", "text-align")] = "center"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
-
- theAttr = {}
- theAttr[_mkTag("fo", "font-size")] = "28pt"
- theAttr[_mkTag("fo", "font-weight")] = "bold"
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ self._mainPara["Title"] = oStyle
# Add Heading 1 Style
# ===================
- theAttr = {}
- theAttr[_mkTag("style", "name")] = "Heading_1"
- theAttr[_mkTag("style", "display-name")] = "Heading 1"
- theAttr[_mkTag("style", "family")] = "paragraph"
- theAttr[_mkTag("style", "parent-style-name")] = "Heading"
- theAttr[_mkTag("style", "next-style-name")] = "Text_body"
- theAttr[_mkTag("style", "default-outline-level")] = "1"
- theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 1")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("1")
+ oStyle.setClass("text")
+ oStyle.setMarginTop("0.423cm")
+ oStyle.setMarginBottom("0.212cm")
+ oStyle.setFontSize("200%")
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_1")
- theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
- theAttr[_mkTag("style", "contextual-spacing")] = "false"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
-
- theAttr = {}
- theAttr[_mkTag("fo", "font-size")] = "130%"
- theAttr[_mkTag("fo", "font-weight")] = "bold"
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ self._mainPara["Heading_1"] = oStyle
# Add Heading 2 Style
# ===================
- theAttr = {}
- theAttr[_mkTag("style", "name")] = "Heading_2"
- theAttr[_mkTag("style", "display-name")] = "Heading 2"
- theAttr[_mkTag("style", "family")] = "paragraph"
- theAttr[_mkTag("style", "parent-style-name")] = "Heading"
- theAttr[_mkTag("style", "next-style-name")] = "Text_body"
- theAttr[_mkTag("style", "default-outline-level")] = "2"
- theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 2")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("2")
+ oStyle.setClass("text")
+ oStyle.setMarginTop("0.353cm")
+ oStyle.setMarginBottom("0.212cm")
+ oStyle.setFontSize("140%")
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_2")
- theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0.353cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
- theAttr[_mkTag("style", "contextual-spacing")] = "false"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
-
- theAttr = {}
- theAttr[_mkTag("fo", "font-size")] = "120%"
- theAttr[_mkTag("fo", "font-weight")] = "bold"
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ self._mainPara["Heading_2"] = oStyle
# Add Heading 3 Style
# ===================
- theAttr = {}
- theAttr[_mkTag("style", "name")] = "Heading_3"
- theAttr[_mkTag("style", "display-name")] = "Heading 3"
- theAttr[_mkTag("style", "family")] = "paragraph"
- theAttr[_mkTag("style", "parent-style-name")] = "Heading"
- theAttr[_mkTag("style", "next-style-name")] = "Text_body"
- theAttr[_mkTag("style", "default-outline-level")] = "3"
- theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 3")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("3")
+ oStyle.setClass("text")
+ oStyle.setMarginTop("0.247cm")
+ oStyle.setMarginBottom("0.212cm")
+ oStyle.setFontSize("125%")
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_3")
- theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0.247cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
- theAttr[_mkTag("style", "contextual-spacing")] = "false"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
-
- theAttr = {}
- theAttr[_mkTag("fo", "font-size")] = "110%"
- theAttr[_mkTag("fo", "font-weight")] = "bold"
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ self._mainPara["Heading_3"] = oStyle
# Add Heading 4 Style
# ===================
- theAttr = {}
- theAttr[_mkTag("style", "name")] = "Heading_4"
- theAttr[_mkTag("style", "display-name")] = "Heading 4"
- theAttr[_mkTag("style", "family")] = "paragraph"
- theAttr[_mkTag("style", "parent-style-name")] = "Heading"
- theAttr[_mkTag("style", "next-style-name")] = "Text_body"
- theAttr[_mkTag("style", "default-outline-level")] = "4"
- theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 4")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("4")
+ oStyle.setClass("text")
+ oStyle.setMarginTop("0.247cm")
+ oStyle.setMarginBottom("0.212cm")
+ oStyle.setFontSize("110%")
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_4")
- theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0.247cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
- theAttr[_mkTag("style", "contextual-spacing")] = "false"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
-
- theAttr = {}
- theAttr[_mkTag("fo", "font-size")] = "100%"
- theAttr[_mkTag("fo", "font-weight")] = "bold"
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ self._mainPara["Heading_4"] = oStyle
return
# END Class ToOdt
-# =========================================================================== #
+# =============================================================================================== #
+# Auto-Style Classes
+# =============================================================================================== #
+
+class ODTParagraphStyle():
+
+ VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
+ VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
+ VALID_LEVEL = ["1", "2", "3", "4"]
+ VALID_CLASS = ["text", "chapter"]
+ VALID_WEIGHT = ["normal", "inherit", "bold"]
+
+ def __init__(self):
+
+ # Attributes
+ self._mAttr = {
+ "display-name": ["style", None],
+ "parent-style-name": ["style", None],
+ "next-style-name": ["style", None],
+ "default-outline-level": ["style", None],
+ "class": ["style", None],
+ }
+
+ # Paragraph Attributes
+ self._pAttr = {
+ "margin-top": ["fo", None],
+ "margin-bottom": ["fo", None],
+ "line-height": ["fo", None],
+ "text-align": ["fo", None],
+ "break-before": ["fo", None],
+ "break-after": ["fo", None],
+ }
+
+ # text Attributes
+ self._tAttr = {
+ "font-size": ["fo", None],
+ "font-weight": ["fo", None],
+ }
+
+ return
+
+ ##
+ # Attribute Setters
+ ##
+
+ def setDisplayName(self, theValue):
+ self._mAttr["display-name"][1] = str(theValue)
+ return
+
+ def setParentStyleName(self, theValue):
+ self._mAttr["parent-style-name"][1] = str(theValue)
+ return
+
+ def setNextStyleName(self, theValue):
+ self._mAttr["next-style-name"][1] = str(theValue)
+ return
+
+ def setOutlineLevel(self, theValue):
+ if theValue in self.VALID_LEVEL:
+ self._mAttr["default-outline-level"][1] = str(theValue)
+ return
+
+ def setClass(self, theValue):
+ if theValue in self.VALID_CLASS:
+ self._mAttr["class"][1] = str(theValue)
+ return
+
+ ##
+ # Paragraph Setters
+ ##
+
+ def setMarginTop(self, theValue):
+ self._pAttr["margin-top"][1] = str(theValue)
+ return
+
+ def setMarginBottom(self, theValue):
+ self._pAttr["margin-bottom"][1] = str(theValue)
+ return
+
+ def setLineHeight(self, theValue):
+ self._pAttr["line-height"][1] = str(theValue)
+ return
+
+ def setTextAlign(self, theValue):
+ if theValue in self.VALID_ALIGN:
+ self._pAttr["text-align"][1] = str(theValue)
+ return
+
+ def setBreakBefore(self, theValue):
+ if theValue in self.VALID_BREAK:
+ self._pAttr["break-before"][1] = str(theValue)
+ return
+
+ def setBreakAfter(self, theValue):
+ if theValue in self.VALID_BREAK:
+ self._pAttr["break-after"][1] = str(theValue)
+ return
+
+ ##
+ # Text Setters
+ ##
+
+ def setFontSize(self, theValue):
+ self._tAttr["font-size"][1] = str(theValue)
+ return
+
+ def setFontWeight(self, theValue):
+ if theValue in self.VALID_WEIGHT:
+ self._tAttr["font-weight"][1] = str(theValue)
+ return
+
+ ##
+ # Methods
+ ##
+
+ def checkNew(self, refStyle):
+ """Check if there are new settings in refStyle that differ from
+ those in the current object.
+ """
+ for aName, (aNm, aVal) in refStyle._mAttr.items():
+ if aVal is not None and aVal != self._mAttr[aName][1]:
+ return True
+ for aName, (aNm, aVal) in refStyle._pAttr.items():
+ if aVal is not None and aVal != self._pAttr[aName][1]:
+ return True
+ for aName, (aNm, aVal) in refStyle._tAttr.items():
+ if aVal is not None and aVal != self._tAttr[aName][1]:
+ return True
+ return False
+
+ def getID(self):
+ """Generate a unique ID from the settings.
+ """
+ theString = (
+ f"Paragraph:Main:{str(self._mAttr)}:"
+ f"Paragraph:Para:{str(self._pAttr)}:"
+ f"Paragraph:Text:{str(self._tAttr)}:"
+ )
+ return sha256(theString.encode()).hexdigest()
+
+ def packXML(self, xParent, xName):
+ """Pack the content into an xml element.
+ """
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = xName
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ for aName, (aNm, aVal) in self._mAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ for aName, (aNm, aVal) in self._pAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ if theAttr:
+ etree.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ for aName, (aNm, aVal) in self._tAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ if theAttr:
+ etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ return
+
+# END Class ODTParagraphStyle
+
+# =============================================================================================== #
# Local Functions
-# =========================================================================== #
+# =============================================================================================== #
def _mkTag(nsName, tagName):
"""Assemble namespace and tag name.
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 6159929a..c0e8c797 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -23,8 +23,25 @@ along with this program. If not, see .
import nw
import pytest
+from lxml import etree
+
from nw.core import NWProject, NWIndex, ToOdt
+XML_NS = [
+ ' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"',
+ ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"',
+ ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"',
+ ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"',
+]
+
+def xmlToText(xElem):
+ """Get the text content of an XML element.
+ """
+ rTxt = etree.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
+ for nSpace in XML_NS:
+ rTxt = rTxt.replace(nSpace, "")
+ return rTxt
+
@pytest.mark.core
def testCoreToOdt_Convert(tmpConf, dummyGUI):
"""Test the converter of the ToHtml class.
@@ -46,6 +63,22 @@ def testCoreToOdt_Convert(tmpConf, dummyGUI):
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
- assert theDoc.theResult == ""
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'Title'
+ ''
+ )
+
+ # Header 1
+ theDoc.theText = "## Chapter Title\n"
+ theDoc.tokenizeText()
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'Chapter Title'
+ ''
+ )
# END Test testCoreToOdt_Convert
From 1683567dda04e67ed26743a096e194062f905db9 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 01:12:34 +0100
Subject: [PATCH 004/104] Restructure Build Novel Project tool to allow ODT
save
---
nw/gui/build.py | 221 ++++++++++++++++++++++++++++--------------------
1 file changed, 130 insertions(+), 91 deletions(-)
diff --git a/nw/gui/build.py b/nw/gui/build.py
index c24edb3e..af7d71d1 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -46,7 +46,7 @@ from PyQt5.QtWidgets import (
from nw.common import fuzzyTime, makeFileNameSafe
from nw.gui.custom import QSwitch
-from nw.core import ToHtml
+from nw.core import ToHtml, ToOdt
from nw.constants import (
nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
)
@@ -56,13 +56,14 @@ logger = logging.getLogger(__name__)
class GuiBuildNovel(QDialog):
FMT_ODT = 1
- FMT_PDF = 2
- FMT_HTM = 3
- FMT_MD = 4
- FMT_NWD = 5
- FMT_TXT = 6
- FMT_JSON_H = 7
- FMT_JSON_M = 8
+ FMT_FODT = 2
+ FMT_PDF = 3
+ FMT_HTM = 4
+ FMT_MD = 5
+ FMT_NWD = 6
+ FMT_TXT = 7
+ FMT_JSON_H = 8
+ FMT_JSON_M = 9
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
@@ -386,6 +387,10 @@ class GuiBuildNovel(QDialog):
self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT))
self.saveMenu.addAction(self.saveODT)
+ self.saveFODT = QAction("Flat Open Document (.fodt)", self)
+ self.saveFODT.triggered.connect(lambda: self._saveDocument(self.FMT_FODT))
+ self.saveMenu.addAction(self.saveFODT)
+
self.savePDF = QAction("Portable Document Format (.pdf)", self)
self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.saveMenu.addAction(self.savePDF)
@@ -533,49 +538,18 @@ class GuiBuildNovel(QDialog):
return True
##
- # Slots
+ # Slots and Related
##
def _buildPreview(self):
"""Build a preview of the project in the document viewer.
"""
# Get Settings
- fmtTitle = self.fmtTitle.text().strip()
- fmtChapter = self.fmtChapter.text().strip()
- fmtUnnumbered = self.fmtUnnumbered.text().strip()
- fmtScene = self.fmtScene.text().strip()
- fmtSection = self.fmtSection.text().strip()
- justifyText = self.justifyText.isChecked()
- noStyling = self.noStyling.isChecked()
- textFont = self.textFont.text()
- textSize = self.textSize.value()
- incSynopsis = self.includeSynopsis.isChecked()
- incComments = self.includeComments.isChecked()
- incKeywords = self.includeKeywords.isChecked()
- novelFiles = self.novelFiles.isChecked()
- noteFiles = self.noteFiles.isChecked()
- ignoreFlag = self.ignoreFlag.isChecked()
- includeBody = self.includeBody.isChecked()
- replaceTabs = self.replaceTabs.isChecked()
-
- makeHtml = ToHtml(self.theProject, self.theParent)
- makeHtml.setTitleFormat(fmtTitle)
- makeHtml.setChapterFormat(fmtChapter)
- makeHtml.setUnNumberedFormat(fmtUnnumbered)
- makeHtml.setSceneFormat(fmtScene, fmtScene == "")
- makeHtml.setSectionFormat(fmtSection, fmtSection == "")
- makeHtml.setBodyText(includeBody)
- makeHtml.setSynopsis(incSynopsis)
- makeHtml.setComments(incComments)
- makeHtml.setKeywords(incKeywords)
- makeHtml.setJustify(justifyText)
- makeHtml.setStyles(not noStyling)
-
- # Make sure the tree order is correct
- self.theParent.treeView.flushTreeOrder()
-
- self.buildProgress.setMaximum(len(self.theProject.projTree))
- self.buildProgress.setValue(0)
+ justifyText = self.justifyText.isChecked()
+ noStyling = self.noStyling.isChecked()
+ textFont = self.textFont.text()
+ textSize = self.textSize.value()
+ replaceTabs = self.replaceTabs.isChecked()
tStart = int(time())
@@ -583,52 +557,8 @@ class GuiBuildNovel(QDialog):
self.htmlStyle = []
self.nwdText = []
- htmlSize = 0
-
- for nItt, tItem in enumerate(self.theProject.projTree):
-
- noteRoot = noteFiles
- noteRoot &= tItem.itemType == nwItemType.ROOT
- noteRoot &= tItem.itemClass != nwItemClass.NOVEL
- noteRoot &= tItem.itemClass != nwItemClass.ARCHIVE
-
- try:
- if noteRoot:
- # Add headers for root folders of notes
- makeHtml.addRootHeading(tItem.itemHandle)
- makeHtml.doConvert()
- self.htmlText.append(makeHtml.getResult())
- self.nwdText.append(makeHtml.getFilteredMarkdown())
- htmlSize += makeHtml.getResultSize()
-
- elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
- makeHtml.setText(tItem.itemHandle)
- makeHtml.doAutoReplace()
- makeHtml.tokenizeText()
- makeHtml.doHeaders()
- makeHtml.doConvert()
- makeHtml.doPostProcessing()
- self.htmlText.append(makeHtml.getResult())
- self.nwdText.append(makeHtml.getFilteredMarkdown())
- htmlSize += makeHtml.getResultSize()
-
- except Exception as e:
- logger.error("Failed to generate html of document '%s'" % tItem.itemHandle)
- logger.error(str(e))
- self.docView.setText((
- "Failed to generate preview. "
- "Document with title '%s' could not be parsed."
- ) % tItem.itemName)
- return False
-
- # Update progress bar, also for skipped items
- self.buildProgress.setValue(nItt+1)
-
- if makeHtml.errData:
- self.theParent.makeAlert((
- "There were problems when building the project:"
- "
- %s"
- ) % "
- ".join(makeHtml.errData), nwAlert.ERROR)
+ makeHtml = ToHtml(self.theProject, self.theParent)
+ htmlSize = self._doBuild(makeHtml)
if replaceTabs:
htmlText = []
@@ -668,6 +598,103 @@ class GuiBuildNovel(QDialog):
return
+ def _doBuild(self, bldObj):
+ """Rund the build with a specific build object.
+ """
+ # Get Settings
+ fmtTitle = self.fmtTitle.text().strip()
+ fmtChapter = self.fmtChapter.text().strip()
+ fmtUnnumbered = self.fmtUnnumbered.text().strip()
+ fmtScene = self.fmtScene.text().strip()
+ fmtSection = self.fmtSection.text().strip()
+ justifyText = self.justifyText.isChecked()
+ noStyling = self.noStyling.isChecked()
+ incSynopsis = self.includeSynopsis.isChecked()
+ incComments = self.includeComments.isChecked()
+ incKeywords = self.includeKeywords.isChecked()
+ novelFiles = self.novelFiles.isChecked()
+ noteFiles = self.noteFiles.isChecked()
+ ignoreFlag = self.ignoreFlag.isChecked()
+ includeBody = self.includeBody.isChecked()
+
+ isHtml = isinstance(bldObj, ToHtml)
+ isOdt = isinstance(bldObj, ToOdt)
+
+ bldObj.setTitleFormat(fmtTitle)
+ bldObj.setChapterFormat(fmtChapter)
+ bldObj.setUnNumberedFormat(fmtUnnumbered)
+ bldObj.setSceneFormat(fmtScene, fmtScene == "")
+ bldObj.setSectionFormat(fmtSection, fmtSection == "")
+ bldObj.setBodyText(includeBody)
+ bldObj.setSynopsis(incSynopsis)
+ bldObj.setComments(incComments)
+ bldObj.setKeywords(incKeywords)
+ bldObj.setJustify(justifyText)
+
+ if isHtml:
+ bldObj.setStyles(not noStyling)
+
+ if isOdt:
+ bldObj.initDocument()
+
+ # Make sure the tree order is correct
+ self.theParent.treeView.flushTreeOrder()
+
+ self.buildProgress.setMaximum(len(self.theProject.projTree))
+ self.buildProgress.setValue(0)
+
+ accSize = 0
+
+ for nItt, tItem in enumerate(self.theProject.projTree):
+
+ noteRoot = noteFiles
+ noteRoot &= tItem.itemType == nwItemType.ROOT
+ noteRoot &= tItem.itemClass != nwItemClass.NOVEL
+ noteRoot &= tItem.itemClass != nwItemClass.ARCHIVE
+
+ try:
+ if noteRoot:
+ # Add headers for root folders of notes
+ bldObj.addRootHeading(tItem.itemHandle)
+ bldObj.doConvert()
+ self.htmlText.append(bldObj.getResult())
+ self.nwdText.append(bldObj.getFilteredMarkdown())
+ accSize += bldObj.getResultSize()
+
+ elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
+ bldObj.setText(tItem.itemHandle)
+ bldObj.doAutoReplace()
+ bldObj.tokenizeText()
+ bldObj.doHeaders()
+ bldObj.doConvert()
+ bldObj.doPostProcessing()
+ self.htmlText.append(bldObj.getResult())
+ self.nwdText.append(bldObj.getFilteredMarkdown())
+ accSize += bldObj.getResultSize()
+
+ except Exception as e:
+ logger.error("Failed to generate html of document '%s'" % tItem.itemHandle)
+ logger.error(str(e))
+ self.docView.setText((
+ "Failed to generate preview. "
+ "Document with title '%s' could not be parsed."
+ ) % tItem.itemName)
+ return False
+
+ # Update progress bar, also for skipped items
+ self.buildProgress.setValue(nItt+1)
+
+ if isOdt:
+ bldObj.closeDocument()
+
+ if bldObj.errData:
+ self.theParent.makeAlert((
+ "There were problems when building the project:"
+ "
- %s"
+ ) % "
- ".join(bldObj.errData), nwAlert.ERROR)
+
+ return accSize
+
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag):
"""This function checks whether a file should be included in the
export or not. For standard note and novel files, this is
@@ -723,6 +750,11 @@ class GuiBuildNovel(QDialog):
textFmt = "Open Document"
outTool = "Qt"
+ elif theFormat == self.FMT_FODT:
+ fileExt = "fodt"
+ textFmt = "Flat Open Document"
+ outTool = "NW2"
+
elif theFormat == self.FMT_PDF:
fileExt = "pdf"
textFmt = "PDF"
@@ -866,6 +898,13 @@ class GuiBuildNovel(QDialog):
except Exception as e:
errMsg = str(e)
+ elif outTool == "NW2":
+
+ if theFormat == self.FMT_FODT:
+ makeOdt = ToOdt(self.theProject, self.theParent)
+ self._doBuild(makeOdt)
+ wSuccess = True
+
elif outTool == "QtPrint" and theFormat == self.FMT_PDF:
try:
thePrinter = QPrinter()
From cbd222fe309ff84a427a38fac081f61afdd31764 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 02:35:19 +0100
Subject: [PATCH 005/104] Add code to save tabs and line breaks to ODT file
---
nw/core/toodt.py | 116 +++++++++++++++++++++++++++++++++++++++++------
nw/gui/build.py | 13 ++++--
2 files changed, 112 insertions(+), 17 deletions(-)
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index c89c065d..48c2765e 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -42,6 +42,9 @@ XML_NS = {
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
+X_BR = "{%s}line-break" % XML_NS["text"]
+X_TAB = "{%s}tab" % XML_NS["text"]
+
class ToOdt(Tokenizer):
def __init__(self, theProject, theParent):
@@ -119,6 +122,9 @@ class ToOdt(Tokenizer):
"""
self.theResult = ""
+ thisPar = []
+ parStyle = None
+ hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Styles
@@ -139,27 +145,35 @@ class ToOdt(Tokenizer):
# Process Text Type
if tType == self.T_EMPTY:
- continue
+ if hasHardBreak and parStyle is not None:
+ if self.doJustify:
+ parStyle.setTextAlign("left")
+ if len(thisPar) > 0:
+ tTemp = "".join(thisPar)
+ self._addTextPar("Text_Body", parStyle, tTemp.rstrip())
+ thisPar = []
+ parStyle = None
+ hasHardBreak = False
elif tType == self.T_TITLE:
- tHead = tText.replace(r"\\", "
")
- self._addTextPar("Title", oStyle, tHead)
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Title", oStyle, tHead, isHead=True)
elif tType == self.T_HEAD1:
- tHead = tText.replace(r"\\", "
")
- self._addTextPar("Heading_1", oStyle, tHead, oLevel="1")
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_1", oStyle, tHead, isHead=True, oLevel="1")
elif tType == self.T_HEAD2:
- tHead = tText.replace(r"\\", "
")
- self._addTextPar("Heading_2", oStyle, tHead, oLevel="2")
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2")
elif tType == self.T_HEAD3:
- tHead = tText.replace(r"\\", "
")
- self._addTextPar("Heading_3", oStyle, tHead, oLevel="3")
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_3", oStyle, tHead, isHead=True, oLevel="3")
elif tType == self.T_HEAD4:
- tHead = tText.replace(r"\\", "
")
- self._addTextPar("Heading_4", oStyle, tHead, oLevel="4")
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_4", oStyle, tHead, isHead=True, oLevel="4")
elif tType == self.T_SEP:
self._addTextPar("Text_Body", oStyle, tText)
@@ -167,6 +181,18 @@ class ToOdt(Tokenizer):
elif tType == self.T_SKIP:
self._addTextPar("Text_Body", oStyle, "")
+ elif tType == self.T_TEXT:
+ tTemp = tText
+ if parStyle is None:
+ parStyle = oStyle
+ # for xPos, xLen, xFmt in reversed(tFormat):
+ # tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
+ if tText.endswith(" "):
+ thisPar.append(tTemp.rstrip()+"\n")
+ hasHardBreak = True
+ else:
+ thisPar.append(tTemp.rstrip()+" ")
+
return
def closeDocument(self):
@@ -192,15 +218,51 @@ class ToOdt(Tokenizer):
# Internal Functions
##
- def _addTextPar(self, styleName, oStyle, theText, oLevel=None):
+ def _addTextPar(self, styleName, oStyle, theText, isHead=False, oLevel=None):
"""Add a text paragraph to the text XML element.
"""
tAttr = {}
tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle)
if oLevel is not None:
tAttr[_mkTag("text", "outline-level")] = oLevel
- xElem = etree.SubElement(self._xText, _mkTag("text", "p"), attrib=tAttr)
- xElem.text = theText
+
+ pTag = "h" if isHead else "p"
+ xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
+
+ if not theText:
+ return
+
+ if "\t" not in theText and "\n" not in theText:
+ xElem.text = theText
+ return
+
+ # Process tabs and line breaks
+ tTemp = ""
+ xTail = None
+ for c in theText:
+ if c == "\t":
+ if xTail is None:
+ xElem.text = tTemp
+ else:
+ xTail.tail = tTemp
+ tTemp = ""
+ xTail = etree.SubElement(xElem, X_TAB)
+ elif c == "\n":
+ if xTail is None:
+ xElem.text = tTemp
+ else:
+ xTail.tail = tTemp
+ tTemp = ""
+ xTail = etree.SubElement(xElem, X_BR)
+ else:
+ tTemp += c
+
+ if tTemp != "":
+ if xTail is None:
+ xElem.text = tTemp
+ else:
+ xTail.tail = tTemp
+
return
def _paraStyle(self, parName, oStyle):
@@ -299,6 +361,10 @@ class ToOdt(Tokenizer):
oStyle.setMarginTop("0cm")
oStyle.setMarginBottom("0.247cm")
oStyle.setLineHeight("115%")
+ if self.doJustify:
+ oStyle.setTextAlign("justify")
+ else:
+ oStyle.setTextAlign("left")
oStyle.packXML(self._xStyl, "Text_Body")
self._mainPara["Text_Body"] = oStyle
@@ -501,6 +567,28 @@ class ODTParagraphStyle():
self._tAttr["font-weight"][1] = str(theValue)
return
+ ##
+ # Getters
+ ##
+
+ def getAttr(self, attrName):
+ """Look through the dictionaries for the value, and return it if
+ we can find it, If not, return None.
+ """
+ retVal = self._mAttr.get(attrName, None)
+ if retVal is not None:
+ return retVal
+
+ retVal = self._pAttr.get(attrName, None)
+ if retVal is not None:
+ return retVal
+
+ retVal = self._tAttr.get(attrName, None)
+ if retVal is not None:
+ return retVal
+
+ return None
+
##
# Methods
##
diff --git a/nw/gui/build.py b/nw/gui/build.py
index af7d71d1..4584d44b 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -753,7 +753,7 @@ class GuiBuildNovel(QDialog):
elif theFormat == self.FMT_FODT:
fileExt = "fodt"
textFmt = "Flat Open Document"
- outTool = "NW2"
+ outTool = "NW_ODT"
elif theFormat == self.FMT_PDF:
fileExt = "pdf"
@@ -898,12 +898,19 @@ class GuiBuildNovel(QDialog):
except Exception as e:
errMsg = str(e)
- elif outTool == "NW2":
+ elif outTool == "NW_ODT":
if theFormat == self.FMT_FODT:
makeOdt = ToOdt(self.theProject, self.theParent)
self._doBuild(makeOdt)
- wSuccess = True
+ try:
+ with open(savePath, mode="wb") as outFile:
+ outFile.write(makeOdt.theResult)
+
+ wSuccess = True
+
+ except Exception as e:
+ errMsg = str(e)
elif outTool == "QtPrint" and theFormat == self.FMT_PDF:
try:
From 8ccd6a8ed3bdec4dd18fdbeeaaa966d73d7d1f9c Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 12:38:31 +0100
Subject: [PATCH 006/104] Fix setting font setting for ODT export
---
nw/core/tokenizer.py | 32 ++++++++---
nw/core/toodt.py | 130 +++++++++++++++++++++++++++++++------------
nw/gui/build.py | 15 ++++-
3 files changed, 130 insertions(+), 47 deletions(-)
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index 6fa7a87a..4e0c6290 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -87,11 +87,15 @@ class Tokenizer():
self.theMarkdown = None # The result text in novelWriter markdown
# User Settings
- self.doBodyText = True # Include body text
- self.doSynopsis = False # Also process synopsis comments
- self.doComments = False # Also process comments
- self.doKeywords = False # Also process keywords like tags and references
- self.doJustify = False # Justify text
+ self.textFont = "Serif" # Output text font
+ self.textSize = 11 # Output text size
+ self.textFixed = False # Fixed width text
+ self.lineHeight = 1.15 # Line height
+ self.doJustify = False # Justify text
+ self.doBodyText = True # Include body text
+ self.doSynopsis = False # Also process synopsis comments
+ self.doComments = False # Also process comments
+ self.doKeywords = False # Also process keywords like tags and references
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
@@ -153,6 +157,20 @@ class Tokenizer():
self.hideSection = hideSection
return
+ def setFont(self, textFont, textSize, textFixed=False):
+ self.textFont = textFont
+ self.textSize = round(int(textSize))
+ self.textFixed = textFixed
+ return
+
+ def setLineHeight(self, lineHeight):
+ self.lineHeight = float(lineHeight)
+ return
+
+ def setJustify(self, doJustify):
+ self.doJustify = doJustify
+ return
+
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
return
@@ -173,10 +191,6 @@ class Tokenizer():
self.doKeywords = doKeywords
return
- def setJustify(self, doJustify):
- self.doJustify = doJustify
- return
-
##
# Class Methods
##
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index 48c2765e..69b92157 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -62,10 +62,25 @@ class ToOdt(Tokenizer):
self._autoPara = {}
self._autoText = {}
- self._dLanguage = "en"
- self._dCountry = "GB"
- self._dFontFace = "Liberation Serif"
- self._dFontSize = 12
+ # Properties
+ self.textFont = "Liberation Serif"
+ self.textSize = 12
+ self.textFixed = False
+
+ # Internal
+ self._fontFamily = None
+ self._fontPitch = "variable"
+ self._fSizeTitle = "30pt"
+ self._fSizeHead1 = "24pt"
+ self._fSizeHead2 = "20pt"
+ self._fSizeHead3 = "16pt"
+ self._fSizeHead4 = "14pt"
+ self._fSizeHead = "14pt"
+ self._fSizeText = "12pt"
+ self._lineHeight = "115%"
+ self._textAlign = "left"
+ self._dLanguage = "en"
+ self._dCountry = "GB"
return
@@ -86,13 +101,6 @@ class ToOdt(Tokenizer):
return True
- def setFont(self, fontFace, fontSize):
- """Set font and font size.
- """
- self._dFontFace = fontFace
- self._dFontSize = fontSize
- return
-
##
# Class Methods
##
@@ -105,11 +113,29 @@ class ToOdt(Tokenizer):
_mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text",
}
self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS)
+ self._xFont = etree.SubElement(self._xRoot, _mkTag("office", "font-face-decls"))
self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles"))
self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles"))
self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body"))
self._xText = etree.SubElement(self._xBody, _mkTag("office", "text"))
+ # Re-Init Variables
+ self._fontFamily = self.textFont
+ if len(self.textFont.split()) > 1:
+ self._fontFamily = f"'{self.textFont}'"
+ self._fontPitch = "fixed" if self.textFixed else "variable"
+
+ self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt"
+ self._fSizeHead1 = f"{round(2.00 * self.textSize):d}pt"
+ self._fSizeHead2 = f"{round(1.60 * self.textSize):d}pt"
+ self._fSizeHead3 = f"{round(1.30 * self.textSize):d}pt"
+ self._fSizeHead4 = f"{round(1.15 * self.textSize):d}pt"
+ self._fSizeHead = f"{round(1.15 * self.textSize):d}pt"
+ self._fSizeText = f"{self.textSize:d}pt"
+
+ self._lineHeight = f"{round(100 * self.lineHeight):d}%"
+ self._textAlign = "justify" if self.doJustify else "left"
+
# Add Styles
self._defaultStyles()
self._useableStyles()
@@ -293,6 +319,14 @@ class ToOdt(Tokenizer):
def _defaultStyles(self):
"""Set the default styles.
"""
+ # Add Font
+ # ========
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = self.textFont
+ theAttr[_mkTag("style", "font-pitch")] = self._fontPitch
+ xStyl = etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=theAttr)
+
# Add Paragraph Family Style
# ==========================
@@ -307,10 +341,11 @@ class ToOdt(Tokenizer):
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
- theAttr[_mkTag("style", "font-name")] = self._dFontFace
- theAttr[_mkTag("fo", "font-size")] = "%dpt" % self._dFontSize
- theAttr[_mkTag("fo", "language")] = self._dLanguage
- theAttr[_mkTag("fo", "country")] = self._dCountry
+ theAttr[_mkTag("style", "font-name")] = self.textFont
+ theAttr[_mkTag("fo", "font-family")] = self._fontFamily
+ theAttr[_mkTag("fo", "font-size")] = self._fSizeText
+ theAttr[_mkTag("fo", "language")] = self._dLanguage
+ theAttr[_mkTag("fo", "country")] = self._dCountry
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Standard Paragraph Style
@@ -320,7 +355,13 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("style", "name")] = "Standard"
theAttr[_mkTag("style", "family")] = "paragraph"
theAttr[_mkTag("style", "class")] = "text"
- etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "font-name")] = self.textFont
+ theAttr[_mkTag("fo", "font-family")] = self._fontFamily
+ theAttr[_mkTag("fo", "font-size")] = self._fSizeText
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Default Heading Style
# =========================
@@ -334,16 +375,15 @@ class ToOdt(Tokenizer):
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
- theAttr[_mkTag("fo", "keep-with-next")] = "always"
+ theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
+ theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("fo", "keep-with-next")] = "always"
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
- theAttr[_mkTag("style", "font-name")] = self._dFontFace
- theAttr[_mkTag("fo", "font-family")] = "'%s'" % self._dFontFace
- theAttr[_mkTag("style", "font-pitch")] = "variable"
- theAttr[_mkTag("fo", "font-size")] = "14pt"
+ theAttr[_mkTag("style", "font-name")] = self.textFont
+ theAttr[_mkTag("fo", "font-family")] = self._fontFamily
+ theAttr[_mkTag("fo", "font-size")] = self._fSizeHead
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
return
@@ -360,11 +400,11 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop("0cm")
oStyle.setMarginBottom("0.247cm")
- oStyle.setLineHeight("115%")
- if self.doJustify:
- oStyle.setTextAlign("justify")
- else:
- oStyle.setTextAlign("left")
+ oStyle.setLineHeight(self._lineHeight)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeText)
+ oStyle.setTextAlign(self._textAlign)
oStyle.packXML(self._xStyl, "Text_Body")
self._mainPara["Text_Body"] = oStyle
@@ -378,7 +418,9 @@ class ToOdt(Tokenizer):
oStyle.setNextStyleName("Text_Body")
oStyle.setClass("chapter")
oStyle.setTextAlign("center")
- oStyle.setFontSize("28pt")
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeTitle)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Title")
@@ -395,7 +437,9 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop("0.423cm")
oStyle.setMarginBottom("0.212cm")
- oStyle.setFontSize("200%")
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead1)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_1")
@@ -412,7 +456,9 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop("0.353cm")
oStyle.setMarginBottom("0.212cm")
- oStyle.setFontSize("140%")
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead2)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_2")
@@ -429,7 +475,9 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop("0.247cm")
oStyle.setMarginBottom("0.212cm")
- oStyle.setFontSize("125%")
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead3)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_3")
@@ -446,7 +494,9 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop("0.247cm")
oStyle.setMarginBottom("0.212cm")
- oStyle.setFontSize("110%")
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead4)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_4")
@@ -491,8 +541,10 @@ class ODTParagraphStyle():
# text Attributes
self._tAttr = {
- "font-size": ["fo", None],
- "font-weight": ["fo", None],
+ "font-name": ["style", None],
+ "font-family": ["fo", None],
+ "font-size": ["fo", None],
+ "font-weight": ["fo", None],
}
return
@@ -558,6 +610,14 @@ class ODTParagraphStyle():
# Text Setters
##
+ def setFontName(self, theValue):
+ self._tAttr["font-name"][1] = str(theValue)
+ return
+
+ def setFontFamily(self, theValue):
+ self._tAttr["font-family"][1] = str(theValue)
+ return
+
def setFontSize(self, theValue):
self._tAttr["font-size"][1] = str(theValue)
return
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 4584d44b..9e80b588 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -35,7 +35,7 @@ from datetime import datetime
from PyQt5.QtCore import Qt, QByteArray, QTimer
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
- QPalette, QColor, QTextDocumentWriter, QFont, QCursor
+ QPalette, QColor, QTextDocumentWriter, QFont, QCursor, QFontInfo
)
from PyQt5.QtWidgets import (
qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
@@ -607,6 +607,8 @@ class GuiBuildNovel(QDialog):
fmtUnnumbered = self.fmtUnnumbered.text().strip()
fmtScene = self.fmtScene.text().strip()
fmtSection = self.fmtSection.text().strip()
+ textFont = self.textFont.text()
+ textSize = self.textSize.value()
justifyText = self.justifyText.isChecked()
noStyling = self.noStyling.isChecked()
incSynopsis = self.includeSynopsis.isChecked()
@@ -617,6 +619,10 @@ class GuiBuildNovel(QDialog):
ignoreFlag = self.ignoreFlag.isChecked()
includeBody = self.includeBody.isChecked()
+ # Get font information
+ fontInfo = QFontInfo(QFont(textFont, textSize))
+ textFixed = fontInfo.fixedPitch()
+
isHtml = isinstance(bldObj, ToHtml)
isOdt = isinstance(bldObj, ToOdt)
@@ -625,11 +631,14 @@ class GuiBuildNovel(QDialog):
bldObj.setUnNumberedFormat(fmtUnnumbered)
bldObj.setSceneFormat(fmtScene, fmtScene == "")
bldObj.setSectionFormat(fmtSection, fmtSection == "")
- bldObj.setBodyText(includeBody)
+
+ bldObj.setFont(textFont, textSize, textFixed)
+ bldObj.setJustify(justifyText)
+
bldObj.setSynopsis(incSynopsis)
bldObj.setComments(incComments)
bldObj.setKeywords(incKeywords)
- bldObj.setJustify(justifyText)
+ bldObj.setBodyText(includeBody)
if isHtml:
bldObj.setStyles(not noStyling)
From cbbe1b4f479a4ea852f4cfac687b549ddf0b5fba Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 13:36:09 +0100
Subject: [PATCH 007/104] Add ODT meta namespace, add font format to HTML, and
merge page break settings
---
nw/core/tohtml.py | 43 +++++++++----------
nw/core/tokenizer.py | 10 ++---
nw/core/toodt.py | 21 ++++++++-
nw/gui/build.py | 2 +-
.../guiBuild_Tool_Step1_Lorem_Ipsum.htm | 3 +-
.../guiBuild_Tool_Step2_Lorem_Ipsum.htm | 3 +-
.../guiBuild_Tool_Step3_Lorem_Ipsum.htm | 3 +-
.../guiBuild_Tool_Step4H_Lorem_Ipsum.json | 5 ++-
.../guiBuild_Tool_Step4_Lorem_Ipsum.htm | 3 +-
tests/test_core/test_core_tohtml.py | 20 +++------
tests/test_core/test_core_tokenizer.py | 2 +-
tests/test_core/test_core_toodt.py | 1 +
12 files changed, 62 insertions(+), 54 deletions(-)
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 46c7d8b6..9c17aeca 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -173,16 +173,12 @@ class ToHtml(Tokenizer):
aStyle.append("text-align: justify;")
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
- if tStyle & self.A_PBB_AV:
- aStyle.append("page-break-before: avoid;")
- if tStyle & self.A_PBB_NO:
- aStyle.append("page-break-before: never;")
+ if tStyle & self.A_PBB_AUT:
+ aStyle.append("page-break-before: auto;")
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
- if tStyle & self.A_PBA_AV:
- aStyle.append("page-break-after: avoid;")
- if tStyle & self.A_PBA_NO:
- aStyle.append("page-break-after: never;")
+ if tStyle & self.A_PBA_AUT:
+ aStyle.append("page-break-after: auto;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
@@ -268,22 +264,23 @@ class ToHtml(Tokenizer):
if not self.cssStyles:
return theStyles
- if self.doJustify:
- theStyles.append(r"p {text-align: justify;}")
- else:
- theStyles.append(r"p {text-align: left;}")
+ textAlign = "justify" if self.doJustify else "left"
- theStyles.append(r"h1, h2 {color: rgb(66, 113, 174);}")
- theStyles.append(r"h3, h4 {color: rgb(50, 50, 50);}")
- theStyles.append(r"h1, h2, h3, h4 {page-break-after: avoid;}")
- theStyles.append(r"a {color: rgb(66, 113, 174);}")
- theStyles.append(r".title {font-size: 2.5em;}")
- theStyles.append(r".tags {color: rgb(245, 135, 31); font-weight: bold;}")
- theStyles.append(r".break {text-align: left;}")
- theStyles.append(r".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}")
- theStyles.append(r".skip {margin-top: 1em; margin-bottom: 1em;}")
- theStyles.append(r".synopsis {font-style: italic;}")
- theStyles.append(r".comment {font-style: italic; color: rgb(100, 100, 100);}")
+ theStyles.append("body {font-family: '%s'; font-size: %dpt}" % (
+ self.textFont, self.textSize)
+ )
+ theStyles.append("p {text-align: %s;}" % textAlign)
+ theStyles.append("h1, h2 {color: rgb(66, 113, 174);}")
+ theStyles.append("h3, h4 {color: rgb(50, 50, 50);}")
+ theStyles.append("h1, h2, h3, h4 {page-break-after: avoid;}")
+ theStyles.append("a {color: rgb(66, 113, 174);}")
+ theStyles.append(".title {font-size: 2.5em;}")
+ theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
+ theStyles.append(".break {text-align: left;}")
+ theStyles.append(".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}")
+ theStyles.append(".skip {margin-top: 1em; margin-bottom: 1em;}")
+ theStyles.append(".synopsis {font-style: italic;}")
+ theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
return theStyles
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index 4e0c6290..04ded5c1 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -67,11 +67,9 @@ class Tokenizer():
A_CENTRE = 0x0004 # Centred
A_JUSTIFY = 0x0008 # Justified
A_PBB = 0x0010 # Page break before always
- A_PBB_AV = 0x0020 # Page break before avoid
- A_PBB_NO = 0x0040 # Page break before never
- A_PBA = 0x0080 # Page break after always
- A_PBA_AV = 0x0100 # Page break after avoid
- A_PBA_NO = 0x0200 # Page break after avoid
+ A_PBB_AUT = 0x0020 # Page break before auto
+ A_PBA = 0x0040 # Page break after always
+ A_PBA_AUT = 0x0080 # Page break after auto
def __init__(self, theProject, theParent):
@@ -628,7 +626,7 @@ class Tokenizer():
tToken[1],
tToken[2],
tToken[3],
- self.A_PBB_NO | self.A_CENTRE
+ self.A_PBB_AUT | self.A_CENTRE
)
else:
self.theTokens[n] = (
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index 69b92157..23721e8e 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -30,6 +30,7 @@ import os
from lxml import etree
from hashlib import sha256
+from datetime import datetime
from nw.core.tokenizer import Tokenizer
@@ -39,6 +40,7 @@ XML_NS = {
"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
+ "meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
@@ -113,12 +115,19 @@ class ToOdt(Tokenizer):
_mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text",
}
self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS)
+ self._xMeta = etree.SubElement(self._xRoot, _mkTag("office", "meta"))
self._xFont = etree.SubElement(self._xRoot, _mkTag("office", "font-face-decls"))
self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles"))
self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles"))
self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body"))
self._xText = etree.SubElement(self._xBody, _mkTag("office", "text"))
+ # Meta Data
+ xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
+ xMeta.text = datetime.now().isoformat()
+ xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
+ xMeta.text = f"novelWriter/{nw.__version__}"
+
# Re-Init Variables
self._fontFamily = self.textFont
if len(self.textFont.split()) > 1:
@@ -166,8 +175,12 @@ class ToOdt(Tokenizer):
oStyle.setTextAlign("justify")
if tStyle & self.A_PBB:
oStyle.setBreakBefore("page")
+ if tStyle & self.A_PBB_AUT:
+ oStyle.setBreakBefore("auto")
if tStyle & self.A_PBA:
oStyle.setBreakAfter("page")
+ if tStyle & self.A_PBA_AUT:
+ oStyle.setBreakAfter("auto")
# Process Text Type
if tType == self.T_EMPTY:
@@ -212,7 +225,7 @@ class ToOdt(Tokenizer):
if parStyle is None:
parStyle = oStyle
# for xPos, xLen, xFmt in reversed(tFormat):
- # tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
+ # tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"\n")
hasHardBreak = True
@@ -511,6 +524,10 @@ class ToOdt(Tokenizer):
# =============================================================================================== #
class ODTParagraphStyle():
+ """Wrapper class for the paragraph style setting used by the
+ exporter. Only the used settings are exposed here to keep the class
+ minimal and fast.
+ """
VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
@@ -539,7 +556,7 @@ class ODTParagraphStyle():
"break-after": ["fo", None],
}
- # text Attributes
+ # Text Attributes
self._tAttr = {
"font-name": ["style", None],
"font-family": ["fo", None],
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 9e80b588..8e2b3dc7 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -573,7 +573,7 @@ class GuiBuildNovel(QDialog):
self.nwdText = nwdText
tEnd = int(time())
- logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
+ logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart)))
self.htmlStyle = makeHtml.getStyleSheet()
self.buildTime = tEnd
diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
index a3e4432f..70b6b8ac 100644
--- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
+++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
@@ -5,6 +5,7 @@
Lorem Ipsum
-Lorem Ipsum
+Lorem Ipsum
By lipsum.com
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm
index 4ab595a3..30d610db 100644
--- a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm
+++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm
@@ -5,6 +5,7 @@
Lorem Ipsum
-Lorem Ipsum
+Lorem Ipsum
By lipsum.com
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm
index 8e6ad4b5..2c385360 100644
--- a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm
+++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm
@@ -5,6 +5,7 @@
Lorem Ipsum
-Lorem Ipsum
+Lorem Ipsum
By lipsum.com
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
diff --git a/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json b/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json
index c67a1321..ede728bb 100644
--- a/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json
+++ b/tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json
@@ -5,10 +5,11 @@
"authors": [
"lipsum.com"
],
- "buildTime": 1611662802
+ "buildTime": 1611750760
},
"text": {
"css": [
+ "body {font-family: 'Sans'; font-size: 12pt}",
"p {text-align: justify;}",
"h1, h2 {color: rgb(66, 113, 174);}",
"h3, h4 {color: rgb(50, 50, 50);}",
@@ -24,7 +25,7 @@
],
"html": [
[
- "Lorem Ipsum
"
+ "Lorem Ipsum
"
],
[
""
diff --git a/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm
index f5360c30..fe3676c8 100644
--- a/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm
+++ b/tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm
@@ -5,6 +5,7 @@
Lorem Ipsum
-Lorem Ipsum
+Lorem Ipsum
Prologue
Synopsis: Explanation from the lipsum.com website.
Act One
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index be99c340..0c20dddd 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -211,12 +211,12 @@ def testCoreToHtml_Convert(dummyGUI):
# Title
theHtml.theTokens = [
- (theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE),
+ (theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_AUT | theHtml.A_CENTRE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
theHtml.doConvert()
assert theHtml.theResult == (
- ""
+ ""
"A Title
\n"
)
@@ -299,24 +299,14 @@ def testCoreToHtml_Convert(dummyGUI):
"style='page-break-before: always; page-break-after: always;'>A Title
\n"
)
- # Page Break Avoid
+ # Page Break Auto
theHtml.theTokens = [
- (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV),
+ (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AUT | theHtml.A_PBA_AUT),
]
theHtml.doConvert()
assert theHtml.theResult == (
"A Title
\n"
- )
-
- # Page Break ANever
- theHtml.theTokens = [
- (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO),
- ]
- theHtml.doConvert()
- assert theHtml.theResult == (
- "A Title
\n"
+ "style='page-break-before: auto; page-break-after: auto;'>A Title\n"
)
# Preview Mode
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index 64096f99..ed5fdc07 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -623,7 +623,7 @@ def testCoreToken_Headers(dummyGUI):
theToken.isPart = False
theToken.doHeaders()
assert theToken.theTokens == [
- (Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE),
+ (Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_AUT | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
]
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index c0e8c797..ae3e0d6b 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -31,6 +31,7 @@ XML_NS = [
' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"',
' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"',
' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"',
+ ' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"',
' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"',
]
From 4df294f0a2e562398769c59c24f19197489df4cc Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 16:22:59 +0100
Subject: [PATCH 008/104] Add customised margin sizes
---
nw/core/tokenizer.py | 33 +++++++++
nw/core/toodt.py | 108 ++++++++++++++++++++++++-----
nw/gui/build.py | 1 +
tests/test_core/test_core_toodt.py | 1 +
4 files changed, 127 insertions(+), 16 deletions(-)
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index 04ded5c1..cc6aeb58 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -95,6 +95,15 @@ class Tokenizer():
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
+ ## Title Margins
+ self.marginTitle = (1.00, 0.50)
+ self.marginHead1 = (1.00, 0.50)
+ self.marginHead2 = (0.85, 0.50)
+ self.marginHead3 = (0.58, 0.50)
+ self.marginHead4 = (0.58, 0.50)
+ self.marginText = (0.00, 0.58)
+
+ ## Title Formats
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
@@ -169,6 +178,30 @@ class Tokenizer():
self.doJustify = doJustify
return
+ def setTitleMargins(self, mUpper, mLower):
+ self.marginTitle = (float(mUpper), float(mLower))
+ return
+
+ def setHead1Margins(self, mUpper, mLower):
+ self.marginHead1 = (float(mUpper), float(mLower))
+ return
+
+ def setHead2Margins(self, mUpper, mLower):
+ self.marginHead2 = (float(mUpper), float(mLower))
+ return
+
+ def setHead3Margins(self, mUpper, mLower):
+ self.marginHead3 = (float(mUpper), float(mLower))
+ return
+
+ def setHead4Margins(self, mUpper, mLower):
+ self.marginHead4 = (float(mUpper), float(mLower))
+ return
+
+ def setTextMargins(self, mUpper, mLower):
+ self.marginText = (float(mUpper), float(mLower))
+ return
+
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
return
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index 23721e8e..e557caf1 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -39,6 +39,7 @@ logger = logging.getLogger(__name__)
XML_NS = {
"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+ "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
@@ -65,12 +66,13 @@ class ToOdt(Tokenizer):
self._autoText = {}
# Properties
- self.textFont = "Liberation Serif"
- self.textSize = 12
- self.textFixed = False
+ self.textFont = "Liberation Serif"
+ self.textSize = 12
+ self.textFixed = False
+ self.colourHead = False
# Internal
- self._fontFamily = None
+ self._fontFamily = "'Liberation Sans'"
self._fontPitch = "variable"
self._fSizeTitle = "30pt"
self._fSizeHead1 = "24pt"
@@ -84,6 +86,29 @@ class ToOdt(Tokenizer):
self._dLanguage = "en"
self._dCountry = "GB"
+ ## Text Margings in Units of em
+ self._mTopTitle = "0.423cm"
+ self._mTopHead1 = "0.423cm"
+ self._mTopHead2 = "0.353cm"
+ self._mTopHead3 = "0.247cm"
+ self._mTopHead4 = "0.247cm"
+ self._mTopHead = "0.423cm"
+ self._mTopText = "0.000cm"
+
+ self._mBotTitle = "0.212cm"
+ self._mBotHead1 = "0.212cm"
+ self._mBotHead2 = "0.212cm"
+ self._mBotHead3 = "0.212cm"
+ self._mBotHead4 = "0.212cm"
+ self._mBotHead = "0.212cm"
+ self._mBotText = "0.247cm"
+
+ ## Colour
+ self._colHead12 = None
+ self._opaHead12 = None
+ self._colHead34 = None
+ self._opaHead34 = None
+
return
##
@@ -103,6 +128,10 @@ class ToOdt(Tokenizer):
return True
+ def setColourHeaders(self, doColour):
+ self.colourHead = doColour
+ return
+
##
# Class Methods
##
@@ -142,6 +171,28 @@ class ToOdt(Tokenizer):
self._fSizeHead = f"{round(1.15 * self.textSize):d}pt"
self._fSizeText = f"{self.textSize:d}pt"
+ self._mTopTitle = self._emToCm(self.marginTitle[0])
+ self._mTopHead1 = self._emToCm(self.marginHead1[0])
+ self._mTopHead2 = self._emToCm(self.marginHead2[0])
+ self._mTopHead3 = self._emToCm(self.marginHead3[0])
+ self._mTopHead4 = self._emToCm(self.marginHead4[0])
+ self._mTopHead = self._emToCm(self.marginHead4[0])
+ self._mTopText = self._emToCm(self.marginText[0])
+
+ self._mBotTitle = self._emToCm(self.marginTitle[1])
+ self._mBotHead1 = self._emToCm(self.marginHead1[1])
+ self._mBotHead2 = self._emToCm(self.marginHead2[1])
+ self._mBotHead3 = self._emToCm(self.marginHead3[1])
+ self._mBotHead4 = self._emToCm(self.marginHead4[1])
+ self._mBotHead = self._emToCm(self.marginHead4[1])
+ self._mBotText = self._emToCm(self.marginText[1])
+
+ if self.colourHead:
+ self._colHead12 = "#2a6099"
+ self._opaHead12 = "100%"
+ self._colHead34 = "#323232"
+ self._opaHead34 = "100%"
+
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
self._textAlign = "justify" if self.doJustify else "left"
@@ -325,6 +376,11 @@ class ToOdt(Tokenizer):
return newName
+ def _emToCm(self, emVal):
+ """Converts an em value to centimetres.
+ """
+ return f"{emVal*2.54/72*self.textSize:.3f}cm"
+
##
# Style Elements
##
@@ -388,8 +444,8 @@ class ToOdt(Tokenizer):
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
- theAttr[_mkTag("fo", "margin-top")] = "0.423cm"
- theAttr[_mkTag("fo", "margin-bottom")] = "0.212cm"
+ theAttr[_mkTag("fo", "margin-top")] = self._mTopHead
+ theAttr[_mkTag("fo", "margin-bottom")] = self._mBotHead
theAttr[_mkTag("fo", "keep-with-next")] = "always"
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
@@ -411,8 +467,8 @@ class ToOdt(Tokenizer):
oStyle.setDisplayName("Text Body")
oStyle.setParentStyleName("Standard")
oStyle.setClass("text")
- oStyle.setMarginTop("0cm")
- oStyle.setMarginBottom("0.247cm")
+ oStyle.setMarginTop(self._mTopText)
+ oStyle.setMarginBottom(self._mBotText)
oStyle.setLineHeight(self._lineHeight)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
@@ -431,6 +487,8 @@ class ToOdt(Tokenizer):
oStyle.setNextStyleName("Text_Body")
oStyle.setClass("chapter")
oStyle.setTextAlign("center")
+ oStyle.setMarginTop(self._mTopTitle)
+ oStyle.setMarginBottom(self._mBotTitle)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeTitle)
@@ -448,11 +506,13 @@ class ToOdt(Tokenizer):
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("1")
oStyle.setClass("text")
- oStyle.setMarginTop("0.423cm")
- oStyle.setMarginBottom("0.212cm")
+ oStyle.setMarginTop(self._mTopHead1)
+ oStyle.setMarginBottom(self._mBotHead1)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead1)
+ oStyle.setColor(self._colHead12)
+ oStyle.setOpacity(self._opaHead12)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_1")
@@ -467,11 +527,13 @@ class ToOdt(Tokenizer):
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("2")
oStyle.setClass("text")
- oStyle.setMarginTop("0.353cm")
- oStyle.setMarginBottom("0.212cm")
+ oStyle.setMarginTop(self._mTopHead2)
+ oStyle.setMarginBottom(self._mBotHead2)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead2)
+ oStyle.setColor(self._colHead12)
+ oStyle.setOpacity(self._opaHead12)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_2")
@@ -486,11 +548,13 @@ class ToOdt(Tokenizer):
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("3")
oStyle.setClass("text")
- oStyle.setMarginTop("0.247cm")
- oStyle.setMarginBottom("0.212cm")
+ oStyle.setMarginTop(self._mTopHead3)
+ oStyle.setMarginBottom(self._mBotHead3)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead3)
+ oStyle.setColor(self._colHead34)
+ oStyle.setOpacity(self._opaHead34)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_3")
@@ -505,11 +569,13 @@ class ToOdt(Tokenizer):
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("4")
oStyle.setClass("text")
- oStyle.setMarginTop("0.247cm")
- oStyle.setMarginBottom("0.212cm")
+ oStyle.setMarginTop(self._mTopHead4)
+ oStyle.setMarginBottom(self._mBotHead4)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead4)
+ oStyle.setColor(self._colHead34)
+ oStyle.setOpacity(self._opaHead34)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_4")
@@ -562,6 +628,8 @@ class ODTParagraphStyle():
"font-family": ["fo", None],
"font-size": ["fo", None],
"font-weight": ["fo", None],
+ "color": ["fo", None],
+ "opacity": ["loext", None],
}
return
@@ -644,6 +712,14 @@ class ODTParagraphStyle():
self._tAttr["font-weight"][1] = str(theValue)
return
+ def setColor(self, theValue):
+ self._tAttr["color"][1] = str(theValue)
+ return
+
+ def setOpacity(self, theValue):
+ self._tAttr["opacity"][1] = str(theValue)
+ return
+
##
# Getters
##
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 8e2b3dc7..09d7d7e9 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -644,6 +644,7 @@ class GuiBuildNovel(QDialog):
bldObj.setStyles(not noStyling)
if isOdt:
+ bldObj.setColourHeaders(not noStyling)
bldObj.initDocument()
# Make sure the tree order is correct
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index ae3e0d6b..300fdd6b 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -30,6 +30,7 @@ from nw.core import NWProject, NWIndex, ToOdt
XML_NS = [
' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"',
' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"',
+ ' xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"',
' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"',
' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"',
' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"',
From 8715ab9322439d361bb2d27f9fbdc5a4d2f9954f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 19:36:34 +0100
Subject: [PATCH 009/104] Add logic to apply formatting to text
---
nw/core/tohtml.py | 6 +-
nw/core/toodt.py | 239 ++++++++++++++++++++++++-----
sample/content/636b6aa9b697b.nwd | 2 +-
tests/test_core/test_core_toodt.py | 41 +++++
4 files changed, 246 insertions(+), 42 deletions(-)
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 9c17aeca..ab7e0c81 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -236,12 +236,12 @@ class ToHtml(Tokenizer):
if parStyle is None:
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
- tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
+ tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
- thisPar.append(tTemp.rstrip()+"
")
+ thisPar.append(tTemp.rstrip() + "
")
hasHardBreak = True
else:
- thisPar.append(tTemp.rstrip()+" ")
+ thisPar.append(tTemp.rstrip() + " ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index e557caf1..2ea5e378 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -26,7 +26,6 @@ along with this program. If not, see .
import nw
import logging
-import os
from lxml import etree
from hashlib import sha256
@@ -45,11 +44,14 @@ XML_NS = {
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
-X_BR = "{%s}line-break" % XML_NS["text"]
-X_TAB = "{%s}tab" % XML_NS["text"]
-
class ToOdt(Tokenizer):
+ X_BLD = 0x01
+ X_ITA = 0x02
+ X_DEL = 0x04
+ X_BRK = 0x08
+ X_TAB = 0x10
+
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
@@ -208,7 +210,17 @@ class ToOdt(Tokenizer):
"""
self.theResult = ""
+ odtTags = {
+ self.FMT_B_B : "_B",
+ self.FMT_B_E : "b_",
+ self.FMT_I_B : "I",
+ self.FMT_I_E : "i",
+ self.FMT_D_B : "_S",
+ self.FMT_D_E : "s_",
+ }
+
thisPar = []
+ thisFmt = []
parStyle = None
hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
@@ -238,10 +250,16 @@ class ToOdt(Tokenizer):
if hasHardBreak and parStyle is not None:
if self.doJustify:
parStyle.setTextAlign("left")
+
if len(thisPar) > 0:
tTemp = "".join(thisPar)
- self._addTextPar("Text_Body", parStyle, tTemp.rstrip())
+ fTemp = "".join(thisFmt)
+ tTxt = tTemp.rstrip()
+ tFmt = fTemp[:len(tTxt)]
+ self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
+
thisPar = []
+ thisFmt = []
parStyle = None
hasHardBreak = False
@@ -275,21 +293,31 @@ class ToOdt(Tokenizer):
tTemp = tText
if parStyle is None:
parStyle = oStyle
- # for xPos, xLen, xFmt in reversed(tFormat):
- # tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
+
+ tFmt = " "*len(tTemp)
+ for xPos, xLen, xFmt in tFormat:
+ tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
+
+ tTxt = tTemp.rstrip()
+ tFmt = tFmt[:len(tTxt)]
if tText.endswith(" "):
- thisPar.append(tTemp.rstrip()+"\n")
+ thisPar.append(tTxt + "\n")
+ thisFmt.append(tFmt + " ")
hasHardBreak = True
else:
- thisPar.append(tTemp.rstrip()+" ")
+ thisPar.append(tTxt + " ")
+ thisFmt.append(tFmt + " ")
return
def closeDocument(self):
"""Return the serialised XML document
"""
+ # Build the auto-generated styles
for styleName, styleObj in self._autoPara.values():
styleObj.packXML(self._xAuto, styleName)
+ for styleName, styleObj in self._autoText.values():
+ styleObj.packXML(self._xAuto, styleName)
self.theResult = etree.tostring(
self._xRoot,
@@ -298,17 +326,13 @@ class ToOdt(Tokenizer):
xml_declaration = True
)
- cacheFile = os.path.join(os.path.expanduser("~"), "Temp", "odtGen.fodt")
- with open(cacheFile, mode="wb") as outFile:
- outFile.write(self.theResult)
-
return
##
# Internal Functions
##
- def _addTextPar(self, styleName, oStyle, theText, isHead=False, oLevel=None):
+ def _addTextPar(self, styleName, oStyle, theText, theFmt="", isHead=False, oLevel=None):
"""Add a text paragraph to the text XML element.
"""
tAttr = {}
@@ -322,36 +346,85 @@ class ToOdt(Tokenizer):
if not theText:
return
- if "\t" not in theText and "\n" not in theText:
- xElem.text = theText
- return
+ ##
+ # Process Formatting
+ ##
- # Process tabs and line breaks
- tTemp = ""
+ if len(theText) != len(theFmt):
+ # Genrate dummu format if there isn't any
+ theFmt = " "*len(theText)
+
+ # XML functions
xTail = None
- for c in theText:
- if c == "\t":
+
+ def appendText(tText):
+ nonlocal xElem, xTail
+ if tText:
if xTail is None:
- xElem.text = tTemp
+ xElem.text = tText
else:
- xTail.tail = tTemp
- tTemp = ""
- xTail = etree.SubElement(xElem, X_TAB)
- elif c == "\n":
- if xTail is None:
- xElem.text = tTemp
- else:
- xTail.tail = tTemp
- tTemp = ""
- xTail = etree.SubElement(xElem, X_BR)
- else:
+ xTail.tail = tText
+
+ def appendSpan(tText, tFmt):
+ nonlocal xElem, xTail
+ if tText:
+ xTail = etree.SubElement(xElem, _mkTag("text", "span"), attrib={
+ _mkTag("text", "style-name"): self._textStyle(tFmt)
+ })
+ xTail.text = tText
+
+ # The formatting loop
+ tTemp = ""
+ xFmt = 0x00
+ pFmt = 0x00
+
+ for i, c in enumerate(theText):
+
+ if theFmt[i] == "_":
+ continue
+ elif theFmt[i] == "B":
+ xFmt |= self.X_BLD
+ elif theFmt[i] == "b":
+ xFmt ^= self.X_BLD
+ elif theFmt[i] == "I":
+ xFmt |= self.X_ITA
+ elif theFmt[i] == "i":
+ xFmt ^= self.X_ITA
+ elif theFmt[i] == "S":
+ xFmt |= self.X_DEL
+ elif theFmt[i] == "s":
+ xFmt ^= self.X_DEL
+
+ if c == "\n":
+ xFmt |= self.X_BRK
+ c = ""
+ elif c == "\t":
+ xFmt |= self.X_TAB
+ c = ""
+
+ if theFmt[i] == " ":
tTemp += c
- if tTemp != "":
- if xTail is None:
- xElem.text = tTemp
- else:
- xTail.tail = tTemp
+ if xFmt != pFmt:
+ if pFmt == 0x00:
+ appendText(tTemp)
+ tTemp = ""
+ else:
+ appendSpan(tTemp, pFmt)
+ tTemp = ""
+
+ if xFmt & self.X_BRK:
+ xTail = etree.SubElement(xElem, _mkTag("text", "line-break"))
+ xFmt ^= self.X_BRK
+
+ if xFmt & self.X_TAB:
+ xTail = etree.SubElement(xElem, _mkTag("text", "tab"))
+ xFmt ^= self.X_TAB
+
+ pFmt = xFmt
+
+ # Save what remains in the buffer
+ appendText(tTemp)
return
@@ -376,6 +449,26 @@ class ToOdt(Tokenizer):
return newName
+ def _textStyle(self, styleCode):
+ """Return a text style for a given style code.
+ """
+ if styleCode in self._autoText:
+ return self._autoText[styleCode][0]
+
+ newName = "T%d" % (len(self._autoText) + 1)
+ newStyle = ODTTextStyle()
+ if styleCode & self.X_BLD:
+ newStyle.setFontWeight("bold")
+ if styleCode & self.X_ITA:
+ newStyle.setFontStyle("italic")
+ if styleCode & self.X_DEL:
+ newStyle.setStrikeStyle("solid")
+ newStyle.setStrikeType("single")
+
+ self._autoText[styleCode] = (newName, newStyle)
+
+ return newName
+
def _emToCm(self, emVal):
"""Converts an em value to centimetres.
"""
@@ -803,6 +896,76 @@ class ODTParagraphStyle():
# END Class ODTParagraphStyle
+class ODTTextStyle():
+ """Wrapper class for the text style setting used by the exporter.
+ Only the used settings are exposed here to keep the class minimal
+ and fast.
+ """
+ VALID_WEIGHT = ["normal", "inherit", "bold"]
+ VALID_STYLE = ["normal", "inherit", "italic"]
+ VALID_LSTYLE = ["none", "solid"]
+ VALID_LTYPE = ["none", "single", "double"]
+
+ def __init__(self):
+
+ # Text Attributes
+ self._tAttr = {
+ "font-weight": ["fo", None],
+ "font-style": ["fo", None],
+ "text-line-through-style": ["style", None],
+ "text-line-through-type": ["style", None],
+ }
+
+ return
+
+ ##
+ # Setters
+ ##
+
+ def setFontWeight(self, theValue):
+ if theValue in self.VALID_WEIGHT:
+ self._tAttr["font-weight"][1] = str(theValue)
+ return
+
+ def setFontStyle(self, theValue):
+ if theValue in self.VALID_STYLE:
+ self._tAttr["font-style"][1] = str(theValue)
+ return
+
+ def setStrikeStyle(self, theValue):
+ if theValue in self.VALID_LSTYLE:
+ self._tAttr["text-line-through-style"][1] = str(theValue)
+ return
+
+ def setStrikeType(self, theValue):
+ if theValue in self.VALID_LTYPE:
+ self._tAttr["text-line-through-type"][1] = str(theValue)
+ return
+
+ ##
+ # Methods
+ ##
+
+ def packXML(self, xParent, xName):
+ """Pack the content into an xml element.
+ """
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = xName
+ theAttr[_mkTag("style", "family")] = "text"
+ xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ for aName, (aNm, aVal) in self._tAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ if theAttr:
+ etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ return
+
+# END Class ODTTextStyle
+
# =============================================================================================== #
# Local Functions
# =============================================================================================== #
diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd
index 6a1302ef..be611dce 100644
--- a/sample/content/636b6aa9b697b.nwd
+++ b/sample/content/636b6aa9b697b.nwd
@@ -9,7 +9,7 @@
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree.
-Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isn’t fully Markdown compliant. If the syntax highlighter doesn’t show it correctly, the export tool will not either.
+Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isn’t fully Markdown compliant. If the syntax highlighter doesn’t show it correctly, the export tool will not either.
In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 300fdd6b..ecf1af8d 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -83,4 +83,45 @@ def testCoreToOdt_Convert(tmpConf, dummyGUI):
''
)
+ # Nested Text
+ theDoc.theText = "Some ~~nested **bold** and _italics_ text~~ text.\nNo format\n"
+ theDoc.tokenizeText()
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'Some '
+ 'nested '
+ 'bold'
+ ' and '
+ 'italics'
+ ' text text. No format'
+ ''
+ )
+
+ # Hard Break
+ theDoc.theText = "Some text. \nNext line\n"
+ theDoc.tokenizeText()
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'Some text.Next line'
+ ''
+ )
+
+ # Tab
+ theDoc.theText = "\tItem 1\tItem 2\n"
+ theDoc.tokenizeText()
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'Item 1Item 2'
+ ''
+ )
+
# END Test testCoreToOdt_Convert
From d83352e16f3c641a49024d84949a87e94f3dcc43 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 27 Jan 2021 19:49:21 +0100
Subject: [PATCH 010/104] Fix Build Novel Project test on Ubuntu
---
tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm | 2 +-
tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.htm | 2 +-
tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.htm | 2 +-
tests/reference/guiBuild_Tool_Step4H_Lorem_Ipsum.json | 2 +-
tests/reference/guiBuild_Tool_Step4_Lorem_Ipsum.htm | 2 +-
tests/test_gui/test_gui_build.py | 3 +++
6 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
index 70b6b8ac..50696774 100644
--- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
+++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
@@ -5,7 +5,7 @@
Lorem Ipsum
\n"
+ "\n"
+ "\n"
+ "{bodyText:s}\n"
+ "\n"
+ "\n"
+ "