diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 58919b30..69dac848 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -52,19 +52,25 @@ X_VERS = "1.2" # Text Formatting Tags TAG_BR = "{%s}line-break" % XML_NS["text"] +TAG_SPC = "{%s}s" % XML_NS["text"] +TAG_NSPC = "{%s}c" % XML_NS["text"] TAG_TAB = "{%s}tab" % XML_NS["text"] TAG_SPAN = "{%s}span" % XML_NS["text"] TAG_STNM = "{%s}style-name" % XML_NS["text"] +# Formatting Codes +X_BLD = 0x01 # Bold format +X_ITA = 0x02 # Italic format +X_DEL = 0x04 # Strikethrough format + +# Formatting Masks +M_BLD = ~X_BLD +M_ITA = ~X_ITA +M_DEL = ~X_DEL + class ToOdt(Tokenizer): - X_BLD = 0x01 # Bold format - X_ITA = 0x02 # Italic format - X_DEL = 0x04 # Strikethrough format - X_BRK = 0x08 # Line break - X_TAB = 0x10 # Tab - def __init__(self, theProject, isFlat): Tokenizer.__init__(self, theProject) @@ -78,24 +84,26 @@ class ToOdt(Tokenizer): self._dStyl = None # ODT styles.xml root self._xMeta = None # Office meta root + self._xFont = None # Office font face declaration + self._xFnt2 = None # Office font face declaration, secondary self._xStyl = None # Office styles root self._xAuto = None # Office auto-styles root + self._xAut2 = None # Office auto-styles root, secondary self._xMast = None # Office master-styles root self._xBody = None # Office body root self._xText = None # Office text root - self._xAut2 = None # Page layout auto-styles for ODT file - self._mainPara = {} # User-accessible paragraph styles self._autoPara = {} # Auto-generated paragraph styles self._autoText = {} # Auto-generated text styles + self._errData = [] # List of errors encountered + # Properties self.textFont = "Liberation Serif" self.textSize = 12 self.textFixed = False self.colourHead = False - self.addHeader = True self.headerText = "" # Internal @@ -176,6 +184,10 @@ class ToOdt(Tokenizer): # Class Methods ## + def getErrors(self): + """Return the list of errors.""" + return self._errData + def initDocument(self): """Initialises a new open document XML tree. """ @@ -275,7 +287,7 @@ class ToOdt(Tokenizer): # content.xml self._dCont = etree.Element(tCont, attrib=tAttr, nsmap=XML_NS) - self._xFnt1 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls")) + self._xFont = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls")) self._xAuto = etree.SubElement(self._dCont, _mkTag("office", "automatic-styles")) self._xBody = etree.SubElement(self._dCont, _mkTag("office", "body")) @@ -290,7 +302,7 @@ class ToOdt(Tokenizer): self._xAut2 = etree.SubElement(self._dStyl, _mkTag("office", "automatic-styles")) self._xMast = etree.SubElement(self._dStyl, _mkTag("office", "master-styles")) - etree.SubElement(self._xFnt1, _mkTag("style", "font-face"), attrib=fAttr) + etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr) etree.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr) # Finalise @@ -554,6 +566,10 @@ class ToOdt(Tokenizer): pTag = "h" if isHead else "p" xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr) + # It's important to set the initial text field to empty, otherwise + # lxml will add a line break if the first subelement is a span. + xElem.text = "" + if not theText: return @@ -565,77 +581,53 @@ class ToOdt(Tokenizer): # Generate an empty format if there isn't any or it doesn't match theFmt = " "*len(theText) - # XML functions - xTail = None - - def appendText(tText): - nonlocal xElem, xTail - if tText: - if xTail is None: - xElem.text = tText - else: - xTail.tail = tText - - def appendSpan(tText, tFmt): - nonlocal xElem, xTail - if tText: - xTail = etree.SubElement(xElem, TAG_SPAN, attrib={ - TAG_STNM: self._textStyle(tFmt) - }) - xTail.text = tText - # The formatting loop tTemp = "" xFmt = 0x00 pFmt = 0x00 + parProc = XMLParagraph(xElem) + for i, c in enumerate(theText): if theFmt[i] == "_": continue elif theFmt[i] == "B": - xFmt |= self.X_BLD + xFmt |= X_BLD elif theFmt[i] == "b": - xFmt ^= self.X_BLD + xFmt &= M_BLD elif theFmt[i] == "I": - xFmt |= self.X_ITA + xFmt |= X_ITA elif theFmt[i] == "i": - xFmt ^= self.X_ITA + xFmt &= M_ITA elif theFmt[i] == "S": - xFmt |= self.X_DEL + xFmt |= 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 = "" + xFmt &= M_DEL if theFmt[i] == " ": tTemp += c if xFmt != pFmt: if pFmt == 0x00: - appendText(tTemp) + parProc.appendText(tTemp) tTemp = "" else: - appendSpan(tTemp, pFmt) + parProc.appendSpan(tTemp, self._textStyle(pFmt)) tTemp = "" - if xFmt & self.X_BRK: - xTail = etree.SubElement(xElem, TAG_BR) - xFmt ^= self.X_BRK - - if xFmt & self.X_TAB: - xTail = etree.SubElement(xElem, TAG_TAB) - xFmt ^= self.X_TAB - pFmt = xFmt # Save what remains in the buffer - appendText(tTemp) + if pFmt == 0x00: + parProc.appendText(tTemp) + else: + parProc.appendSpan(tTemp, self._textStyle(pFmt)) + + nErr, errMsg = parProc.checkError() + if nErr > 0: # pragma: no cover + # This one should only capture bugs + self._errData.append(errMsg) return @@ -668,11 +660,11 @@ class ToOdt(Tokenizer): newName = "T%d" % (len(self._autoText) + 1) newStyle = ODTTextStyle() - if styleCode & self.X_BLD: + if styleCode & X_BLD: newStyle.setFontWeight("bold") - if styleCode & self.X_ITA: + if styleCode & X_ITA: newStyle.setFontStyle("italic") - if styleCode & self.X_DEL: + if styleCode & X_DEL: newStyle.setStrikeStyle("solid") newStyle.setStrikeType("single") @@ -781,9 +773,6 @@ class ToOdt(Tokenizer): # Add Header and Footer Styles # ============================ - if not self.addHeader: - return - theAttr = {} theAttr[_mkTag("style", "name")] = "Header_and_Footer" theAttr[_mkTag("style", "display-name")] = "Header and Footer" @@ -939,9 +928,6 @@ class ToOdt(Tokenizer): # Add Header Style # ================ - if not self.addHeader: - return - oStyle = ODTParagraphStyle() oStyle.setDisplayName("Header") oStyle.setParentStyleName("Header_and_Footer") @@ -955,9 +941,6 @@ class ToOdt(Tokenizer): def _writeHeader(self): """Write the header elements. """ - if not self.addHeader: - return - theAttr = {} theAttr[_mkTag("style", "name")] = "Standard" theAttr[_mkTag("style", "page-layout-name")] = "PM1" @@ -1055,11 +1038,15 @@ class ODTParagraphStyle(): def setOutlineLevel(self, theValue): if theValue in self.VALID_LEVEL: self._mAttr["default-outline-level"][1] = str(theValue) + else: + self._mAttr["default-outline-level"][1] = None return def setClass(self, theValue): if theValue in self.VALID_CLASS: self._mAttr["class"][1] = str(theValue) + else: + self._mAttr["class"][1] = None return ## @@ -1089,16 +1076,22 @@ class ODTParagraphStyle(): def setTextAlign(self, theValue): if theValue in self.VALID_ALIGN: self._pAttr["text-align"][1] = str(theValue) + else: + self._pAttr["text-align"][1] = None return def setBreakBefore(self, theValue): if theValue in self.VALID_BREAK: self._pAttr["break-before"][1] = str(theValue) + else: + self._pAttr["break-before"][1] = None return def setBreakAfter(self, theValue): if theValue in self.VALID_BREAK: self._pAttr["break-after"][1] = str(theValue) + else: + self._pAttr["break-after"][1] = None return ## @@ -1120,6 +1113,8 @@ class ODTParagraphStyle(): def setFontWeight(self, theValue): if theValue in self.VALID_WEIGHT: self._tAttr["font-weight"][1] = str(theValue) + else: + self._tAttr["font-weight"][1] = None return def setColor(self, theValue): @@ -1130,28 +1125,6 @@ class ODTParagraphStyle(): self._tAttr["opacity"][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 ## @@ -1243,21 +1216,29 @@ class ODTTextStyle(): def setFontWeight(self, theValue): if theValue in self.VALID_WEIGHT: self._tAttr["font-weight"][1] = str(theValue) + else: + self._tAttr["font-weight"][1] = None return def setFontStyle(self, theValue): if theValue in self.VALID_STYLE: self._tAttr["font-style"][1] = str(theValue) + else: + self._tAttr["font-style"][1] = None return def setStrikeStyle(self, theValue): if theValue in self.VALID_LSTYLE: self._tAttr["text-line-through-style"][1] = str(theValue) + else: + self._tAttr["text-line-through-style"][1] = None return def setStrikeType(self, theValue): if theValue in self.VALID_LTYPE: self._tAttr["text-line-through-type"][1] = str(theValue) + else: + self._tAttr["text-line-through-type"][1] = None return ## @@ -1285,6 +1266,203 @@ class ODTTextStyle(): # END Class ODTTextStyle +# =============================================================================================== # +# XML Complex Element Helper Class +# =============================================================================================== # + +X_ROOT_TEXT = 0 +X_ROOT_TAIL = 1 +X_SPAN_TEXT = 2 +X_SPAN_SING = 3 + + +class XMLParagraph(): + """This is a helper class to manage the text content of a single + XML element using mixed content tags. + + See: https://lxml.de/tutorial.html#the-element-class + + Rules: + * The root tag can only have text set, never tail. + * Any span must be under root, and the text in the span is set in + one pass. The span then becomes the new base element using tail + for further added text, permanently replacing root. + * Any single special tags like tabs, line breaks or multi-spaces, + should never have text set. After insertion, they become the next + tail tag if on root level, or if in a span, only exists within + the lifetime of the span. In this case, the span becomes the new + tail. + + The four constants associated with this class represent the only + allowed states the class can exist in, which dictates which XML + object and attribute is written to, + """ + + def __init__(self, xRoot): + + self._xRoot = xRoot + self._xTail = None + self._xSing = None + + self._nState = X_ROOT_TEXT + self._chrPos = 0 + self._rawTxt = "" + self._xRoot.text = "" + + return + + def appendText(self, tText): + """Append text to the XML element. We do this one character at + the time in order to be able to process line breaks, tabs and + spaces separately. Multiple spaces above one are concatenated + into a single tag, and must therefore be processed separately. + """ + nSpaces = 0 + self._rawTxt += tText + + for c in tText: + if c == " ": + nSpaces += 1 + continue + + elif nSpaces > 0: + self._processSpaces(nSpaces) + nSpaces = 0 + + if c == "\n": + if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL): + self._xTail = etree.SubElement(self._xRoot, TAG_BR) + self._xTail.tail = "" + self._nState = X_ROOT_TAIL + self._chrPos += 1 + + elif self._nState in (X_SPAN_TEXT, X_SPAN_SING): + self._xSing = etree.SubElement(self._xTail, TAG_BR) + self._xSing.tail = "" + self._nState = X_SPAN_SING + self._chrPos += 1 + + elif c == "\t": + if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL): + self._xTail = etree.SubElement(self._xRoot, TAG_TAB) + self._xTail.tail = "" + self._nState = X_ROOT_TAIL + self._chrPos += 1 + + elif self._nState in (X_SPAN_TEXT, X_SPAN_SING): + self._xSing = etree.SubElement(self._xTail, TAG_TAB) + self._xSing.tail = "" + self._chrPos += 1 + self._nState = X_SPAN_SING + + else: + if self._nState == X_ROOT_TEXT: + self._xRoot.text += c + self._chrPos += 1 + elif self._nState == X_ROOT_TAIL: + self._xTail.tail += c + self._chrPos += 1 + elif self._nState == X_SPAN_TEXT: + self._xTail.text += c + self._chrPos += 1 + elif self._nState == X_SPAN_SING: + self._xSing.tail += c + self._chrPos += 1 + + if nSpaces > 0: + self._processSpaces(nSpaces) + + return + + def appendSpan(self, tText, tFmt): + """Append a text span to the XML element. The span is always + closed since we do not allow nested spans (like Libre Office). + Therefore we return to the root element level when we're done + processing the text of the span. + """ + self._xTail = etree.SubElement(self._xRoot, TAG_SPAN, attrib={ + TAG_STNM: tFmt + }) + self._xTail.text = "" # Defaults to None + self._xTail.tail = "" # Defaults to None + self._nState = X_SPAN_TEXT + self.appendText(tText) + self._nState = X_ROOT_TAIL + + return + + def checkError(self): + """Check that the number of characters written matches the + number of characters received.""" + errMsg = "" + nMissed = len(self._rawTxt) - self._chrPos + if nMissed != 0: + errMsg = "%d char(s) were not written: '%s'" % (nMissed, self._rawTxt) + return nMissed, errMsg + + ## + # Internal Functions + ## + + def _processSpaces(self, nSpaces): + """Add spaces to paragraph. The first space is always written + as-is (unless it's the first character of the paragraph). The + second space uses the dedicated tag for spaces, and from the + third space and on, a counter is added to the tag. + + See: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html + Sections: 6.1.2, 6.1.3, and 19.763 + """ + if nSpaces > 0: + if self._chrPos > 0: + if self._nState == X_ROOT_TEXT: + self._xRoot.text += " " + self._chrPos += 1 + elif self._nState == X_ROOT_TAIL: + self._xTail.tail += " " + self._chrPos += 1 + elif self._nState == X_SPAN_TEXT: + self._xTail.text += " " + self._chrPos += 1 + elif self._nState == X_SPAN_SING: + self._xSing.tail += " " + self._chrPos += 1 + else: + nSpaces += 1 + + if nSpaces == 2: + if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL): + self._xTail = etree.SubElement(self._xRoot, TAG_SPC) + self._xTail.tail = "" + self._nState = X_ROOT_TAIL + self._chrPos += nSpaces - 1 + + elif self._nState in (X_SPAN_TEXT, X_SPAN_SING): + self._xSing = etree.SubElement(self._xTail, TAG_SPC) + self._xSing.tail = "" + self._nState = X_SPAN_SING + self._chrPos += nSpaces - 1 + + elif nSpaces > 2: + if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL): + self._xTail = etree.SubElement(self._xRoot, TAG_SPC, attrib={ + TAG_NSPC: str(nSpaces - 1) + }) + self._xTail.tail = "" + self._nState = X_ROOT_TAIL + self._chrPos += nSpaces - 1 + + elif self._nState in (X_SPAN_TEXT, X_SPAN_SING): + self._xSing = etree.SubElement(self._xTail, TAG_SPC, attrib={ + TAG_NSPC: str(nSpaces - 1) + }) + self._xSing.tail = "" + self._nState = X_SPAN_SING + self._chrPos += nSpaces - 1 + + return + + # =============================================================================================== # # Local Functions # =============================================================================================== # diff --git a/sample/content/974e400180a99.nwd b/sample/content/974e400180a99.nwd index 266563ae..ff9b71d5 100644 --- a/sample/content/974e400180a99.nwd +++ b/sample/content/974e400180a99.nwd @@ -6,4 +6,4 @@ This is a plain page with some text on it. -If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the ]VSPACE] code. +If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index dd91533c..54bd9f17 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1136 - 194 - 54509 + 1141 + 195 + 54687 False @@ -17,8 +17,8 @@ True 636b6aa9b697b 636b6aa9b697b - 1209 - 833 + 1206 + 830 376 B @@ -75,10 +75,10 @@ New True DOCUMENT - 206 - 42 - 3 - 21 + 186 + 39 + 2 + 212 Part One @@ -121,7 +121,7 @@ 2429 432 14 - 813 + 61 Another Scene diff --git a/tests/reference/coreToOdt_SaveFlat_document.fodt b/tests/reference/coreToOdt_SaveFlat_document.fodt new file mode 100644 index 00000000..8892e14a --- /dev/null +++ b/tests/reference/coreToOdt_SaveFlat_document.fodt @@ -0,0 +1,81 @@ + + + + 2021-08-13T18:57:51 + novelWriter/1.5-alpha0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + / / 2 + + + + + + + + + Chapter One + Text + Chapter Two + Text + + + diff --git a/tests/reference/coreToOdt_SaveFull_content.xml b/tests/reference/coreToOdt_SaveFull_content.xml new file mode 100644 index 00000000..8a20204f --- /dev/null +++ b/tests/reference/coreToOdt_SaveFull_content.xml @@ -0,0 +1,15 @@ + + + + + + + + + Chapter One + Text + Chapter Two + Text + + + diff --git a/tests/reference/coreToOdt_SaveFull_manifest.xml b/tests/reference/coreToOdt_SaveFull_manifest.xml new file mode 100644 index 00000000..df81d9bc --- /dev/null +++ b/tests/reference/coreToOdt_SaveFull_manifest.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/reference/coreToOdt_SaveFull_meta.xml b/tests/reference/coreToOdt_SaveFull_meta.xml new file mode 100644 index 00000000..f280ad49 --- /dev/null +++ b/tests/reference/coreToOdt_SaveFull_meta.xml @@ -0,0 +1,7 @@ + + + + 2021-08-13T18:54:46 + novelWriter/1.5-alpha0 + + diff --git a/tests/reference/coreToOdt_SaveFull_settings.xml b/tests/reference/coreToOdt_SaveFull_settings.xml new file mode 100644 index 00000000..96545148 --- /dev/null +++ b/tests/reference/coreToOdt_SaveFull_settings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/reference/coreToOdt_SaveFull_styles.xml b/tests/reference/coreToOdt_SaveFull_styles.xml new file mode 100644 index 00000000..79ddae7f --- /dev/null +++ b/tests/reference/coreToOdt_SaveFull_styles.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + / / 2 + + + + + + + diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt index a1e93752..a69a868f 100644 --- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt +++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt @@ -1,8 +1,8 @@ - 2021-07-26T23:47:22 - novelWriter/1.4rc1 + 2021-08-02T23:47:45 + novelWriter/1.5-alpha0 @@ -88,9 +88,7 @@ Lorem Ipsum - - By lipsum.com - + By lipsum.com “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt index 2c9a83ac..4ae42e2c 100644 --- a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt +++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt @@ -1,7 +1,7 @@ - 2021-08-02T03:24:17 + 2021-08-02T23:48:04 novelWriter/1.5-alpha0 @@ -100,9 +100,7 @@ Lorem Ipsum - - By lipsum.com - + By lipsum.com “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” Comment: Exctracted from the lipsum.com website. diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt index fe483d20..d335a9a9 100644 --- a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt +++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt @@ -1,7 +1,7 @@ - 2021-08-02T03:25:21 + 2021-08-02T23:48:26 novelWriter/1.5-alpha0 @@ -100,9 +100,7 @@ Lorem Ipsum - - By lipsum.com - + By lipsum.com “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” Comment: Exctracted from the lipsum.com website. diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 103aa21f..fd6f1c0b 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -19,11 +19,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os import pytest +import zipfile from lxml import etree +from shutil import copyfile + +from tools import cmpFiles from nw.core import NWProject, NWIndex, ToOdt +from nw.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag XML_NS = [ ' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"', @@ -45,48 +51,290 @@ def xmlToText(xElem): @pytest.mark.core -def testCoreToOdt_Convert(mockGUI): - """Test the converter of the ToHtml class. +def testCoreToOdt_Init(mockGUI): + """Test initialisation of the ODT document. + """ + theProject = NWProject(mockGUI) + mockGUI.theIndex = NWIndex(theProject) + + # Flat Doc + # ======== + + theDoc = ToOdt(theProject, isFlat=True) + theDoc.initDocument() + + # Document XML + assert theDoc._dFlat is not None + assert theDoc._dCont is None + assert theDoc._dMeta is None + assert theDoc._dStyl is None + + # Content XML + assert theDoc._xMeta is not None + assert theDoc._xFont is not None + assert theDoc._xFnt2 is None + assert theDoc._xStyl is not None + assert theDoc._xAuto is not None + assert theDoc._xAut2 is None + assert theDoc._xMast is not None + assert theDoc._xBody is not None + assert theDoc._xText is not None + + # ODT Doc + # ======= + + theDoc = ToOdt(theProject, isFlat=False) + theDoc.initDocument() + + # Document XML + assert theDoc._dFlat is None + assert theDoc._dCont is not None + assert theDoc._dMeta is not None + assert theDoc._dStyl is not None + + # Content XML + assert theDoc._xMeta is not None + assert theDoc._xFont is not None + assert theDoc._xFnt2 is not None + assert theDoc._xStyl is not None + assert theDoc._xAuto is not None + assert theDoc._xAut2 is not None + assert theDoc._xMast is not None + assert theDoc._xBody is not None + assert theDoc._xText is not None + +# END Test testCoreToOdt_Init + + +@pytest.mark.core +def testCoreToOdt_TextFormatting(mockGUI): + """Test formatting of paragraphs. """ theProject = NWProject(mockGUI) mockGUI.theIndex = NWIndex(theProject) theDoc = ToOdt(theProject, isFlat=True) - # Export Mode - # =========== + theDoc.initDocument() + assert xmlToText(theDoc._xText) == "" + + # Paragraph Style + # =============== + oStyle = ODTParagraphStyle() + + assert theDoc._paraStyle("stuff", oStyle) == "Standard" + assert theDoc._paraStyle("Text_Body", oStyle) == "Text_Body" + + # Create new para style + oStyle.setTextAlign("center") + assert theDoc._paraStyle("Text_Body", oStyle) == "P1" + + # Return the same style on second call + assert theDoc._paraStyle("Text_Body", oStyle) == "P1" + + assert list(theDoc._mainPara.keys()) == [ + "Text_Body", "Text_Meta", "Title", "Heading_1", + "Heading_2", "Heading_3", "Heading_4", "Header" + ] + + theKey = "a956b3abcc3d2d5daedf829b2cef56b4ba9b5b583c9c8cc8c87816dfc5a0685d" + assert theDoc._autoPara[theKey][0] == "P1" + assert isinstance(theDoc._autoPara[theKey][1], ODTParagraphStyle) + + # Paragraph Formatting + # ==================== + oStyle = ODTParagraphStyle() + + # No Text + theDoc.initDocument() + theDoc._addTextPar("Standard", oStyle, "") + assert xmlToText(theDoc._xText) == ( + "" + "" + "" + ) + + # No Format + theDoc.initDocument() + theDoc._addTextPar("Standard", oStyle, "Hello World") + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + "" + "Hello World" + "" + ) + + # Heading Level None + theDoc.initDocument() + theDoc._addTextPar("Standard", oStyle, "Hello World", isHead=True) + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + "" + "Hello World" + "" + ) + + # Heading Level 1 + theDoc.initDocument() + theDoc._addTextPar("Standard", oStyle, "Hello World", isHead=True, oLevel="1") + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + "" + "Hello World" + "" + ) + + # Formatted Text + theDoc.initDocument() + theTxt = "A **few** _words_ from ~~our~~ sponsor" + theFmt = " _B b_ I i _S s_ " + theDoc._addTextPar("Standard", oStyle, theTxt, theFmt=theFmt) + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + "" + "A few " + "words from our sponsor" + "" + ) + + # Incorrectly Formatted Text + theDoc.initDocument() + theTxt = "A **few** _words" + theFmt = " _b b_ I " + theDoc._addTextPar("Standard", oStyle, theTxt, theFmt=theFmt) + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + "" + "" + "A few words" + "" + "" + ) + + # Formatted Text + theDoc.initDocument() + theTxt = "Hello\n\tWorld" + theFmt = " " + theDoc._addTextPar("Standard", oStyle, theTxt, theFmt=theFmt) + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + "" + "HelloWorld" + "" + ) + + # Tabs and Breaks + +# END Test testCoreToOdt_TextFormatting + + +@pytest.mark.core +def testCoreToOdt_Convert(mockGUI): + """Test the converter of the ToOdt class. + """ + theProject = NWProject(mockGUI) + mockGUI.theIndex = NWIndex(theProject) + theDoc = ToOdt(theProject, isFlat=True) theDoc.isNovel = True + def getStyle(styleName): + for aSet in theDoc._autoPara.values(): + if aSet[0] == styleName: + return aSet[1] + return None + + # Headers + # ======= + # Header 1 theDoc.theText = "# Title\n" theDoc.tokenizeText() theDoc.initDocument() theDoc.doConvert() theDoc.closeDocument() + assert theDoc.getErrors() == [] assert xmlToText(theDoc._xText) == ( '' 'Title' '' ) - # Header 1 - theDoc.theText = "## Chapter Title\n" + # Header 2 + theDoc.theText = "## Chapter\n" theDoc.tokenizeText() theDoc.initDocument() theDoc.doConvert() theDoc.closeDocument() + assert theDoc.getErrors() == [] assert xmlToText(theDoc._xText) == ( '' - 'Chapter Title' + 'Chapter' '' ) - # Nested Text - theDoc.theText = "Some ~~nested **bold** and _italics_ text~~ text.\nNo format\n" + # Header 3 + theDoc.theText = "### Scene\n" theDoc.tokenizeText() theDoc.initDocument() theDoc.doConvert() theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Scene' + '' + ) + + # Header 4 + theDoc.theText = "#### Section\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Section' + '' + ) + + # Title + theDoc.theText = "#! Title\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Title' + '' + ) + + # Unnumbered chapter + theDoc.theText = "##! Prologue\n" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Prologue' + '' + ) + + # Paragraphs + # ========== + + # Nested Text + theDoc.theText = "Some ~~nested **bold** and _italics_ text~~ text." + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] assert xmlToText(theDoc._xText) == ( '' 'Some ' @@ -94,17 +342,17 @@ def testCoreToOdt_Convert(mockGUI): 'bold' ' and ' 'italics' - ' text text.' - 'No format' + ' text text.' '' ) # Hard Break - theDoc.theText = "Some text. \nNext line\n" + theDoc.theText = "Some text.\nNext line\n" theDoc.tokenizeText() theDoc.initDocument() theDoc.doConvert() theDoc.closeDocument() + assert theDoc.getErrors() == [] assert xmlToText(theDoc._xText) == ( '' 'Some text.Next line' @@ -117,10 +365,820 @@ def testCoreToOdt_Convert(mockGUI): theDoc.initDocument() theDoc.doConvert() theDoc.closeDocument() + assert theDoc.getErrors() == [] assert xmlToText(theDoc._xText) == ( '' 'Item 1Item 2' '' ) + # Tab in Format + theDoc.theText = "Some **bold\ttext**" + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Some ' + 'boldtext' + '' + ) + + # Multiple Spaces + theDoc.theText = ( + "### Scene\n\n" + "Hello World\n\n" + "Hello World\n\n" + "Hello World\n\n" + ) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Scene' + 'Hello World' + 'Hello World' + 'Hello World' + '' + ) + + # Synopsis, Comment, Keywords + theDoc.theText = ( + "### Scene\n\n" + "@pov: Jane\n\n" + "% synopsis: So it begins\n\n" + "% a plain comment\n\n" + ) + theDoc.setSynopsis(True) + theDoc.setComments(True) + theDoc.setKeywords(True) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Scene' + '' + 'Point of View: Jane' + '' + 'Synopsis: So it begins' + '' + 'Comment: a plain comment' + '' + ) + + # Scene Separator + theDoc.theText = "### Scene One\n\nText\n\n### Scene Two\n\nText" + theDoc.setSceneFormat("* * *", False) + theDoc.tokenizeText() + theDoc.doHeaders() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + '* * *' + 'Text' + '* * *' + 'Text' + '' + ) + + # Scene Break + theDoc.theText = "### Scene One\n\nText\n\n### Scene Two\n\nText" + theDoc.setSceneFormat("", False) + theDoc.tokenizeText() + theDoc.doHeaders() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + '' + 'Text' + '' + 'Text' + '' + ) + + # Paragraph Styles + theDoc.theText = ( + "### Scene\n\n" + "@pov: Jane\n" + "@char: John\n" + "@plot: Main\n\n" + ">> Right align\n\n" + "Left Align <<\n\n" + ">> Centered <<\n\n" + "> Left indent\n\n" + "Right indent <\n\n" + ) + theDoc.setKeywords(True) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Scene' + '' + 'Point of View: Jane' + '' + 'Characters: John' + '' + 'Plot: Main' + 'Right align' + 'Left Align' + 'Centered' + 'Left indent' + 'Right indent' + '' + ) + assert getStyle("P5")._pAttr["margin-bottom"] == ["fo", "0.000cm"] + assert getStyle("P6")._pAttr["margin-bottom"] == ["fo", "0.000cm"] + assert getStyle("P6")._pAttr["margin-top"] == ["fo", "0.000cm"] + assert getStyle("P7")._pAttr["text-align"] == ["fo", "right"] + assert getStyle("P4")._pAttr["text-align"] == ["fo", "center"] + assert getStyle("P8")._pAttr["margin-left"] == ["fo", "1.693cm"] + assert getStyle("P9")._pAttr["margin-right"] == ["fo", "1.693cm"] + + # Justified + theDoc.theText = ( + "### Scene\n\n" + "Regular paragraph\n\n" + "with\nbreak\n\n" + "Left Align <<\n\n" + ) + theDoc.setJustify(True) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Scene' + 'Regular paragraph' + 'withbreak' + 'Left Align' + '' + ) + assert getStyle("P10")._pAttr["text-align"] == ["fo", "left"] + + # Page Breaks + theDoc.theText = ( + "## Chapter One\n\n" + "Text\n\n" + "## Chapter Two\n\n" + "Text\n\n" + ) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + assert theDoc.getErrors() == [] + assert xmlToText(theDoc._xText) == ( + '' + 'Chapter One' + 'Text' + 'Chapter Two' + 'Text' + '' + ) + # END Test testCoreToOdt_Convert + + +@pytest.mark.core +def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): + """Test the document save functions. + """ + theProject = NWProject(mockGUI) + mockGUI.theIndex = NWIndex(theProject) + + theDoc = ToOdt(theProject, isFlat=True) + theDoc.isNovel = True + assert theDoc.setLanguage(None) is False + assert theDoc.setLanguage("nb_NO") is True + theDoc.setColourHeaders(True) + + theDoc.theText = ( + "## Chapter One\n\n" + "Text\n\n" + "## Chapter Two\n\n" + "Text\n\n" + ) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + + flatFile = os.path.join(fncDir, "document.fodt") + testFile = os.path.join(outDir, "coreToOdt_SaveFlat_document.fodt") + compFile = os.path.join(refDir, "coreToOdt_SaveFlat_document.fodt") + + theDoc.saveFlatXML(flatFile) + assert os.path.isfile(flatFile) + + copyfile(flatFile, testFile) + assert cmpFiles(testFile, compFile, [4, 5]) + +# END Test testCoreToOdt_SaveFlat + + +@pytest.mark.core +def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir): + """Test the document save functions. + """ + theProject = NWProject(mockGUI) + mockGUI.theIndex = NWIndex(theProject) + + theDoc = ToOdt(theProject, isFlat=False) + theDoc.isNovel = True + + theDoc.theText = ( + "## Chapter One\n\n" + "Text\n\n" + "## Chapter Two\n\n" + "Text\n\n" + ) + theDoc.tokenizeText() + theDoc.initDocument() + theDoc.doConvert() + theDoc.closeDocument() + + fullFile = os.path.join(fncDir, "document.odt") + + theDoc.saveOpenDocText(fullFile) + assert os.path.isfile(fullFile) + assert zipfile.is_zipfile(fullFile) + + maniFile = os.path.join(outDir, "coreToOdt_SaveFull_manifest.xml") + settFile = os.path.join(outDir, "coreToOdt_SaveFull_settings.xml") + contFile = os.path.join(outDir, "coreToOdt_SaveFull_content.xml") + metaFile = os.path.join(outDir, "coreToOdt_SaveFull_meta.xml") + stylFile = os.path.join(outDir, "coreToOdt_SaveFull_styles.xml") + + maniComp = os.path.join(refDir, "coreToOdt_SaveFull_manifest.xml") + settComp = os.path.join(refDir, "coreToOdt_SaveFull_settings.xml") + contComp = os.path.join(refDir, "coreToOdt_SaveFull_content.xml") + metaComp = os.path.join(refDir, "coreToOdt_SaveFull_meta.xml") + stylComp = os.path.join(refDir, "coreToOdt_SaveFull_styles.xml") + + extaxtTo = os.path.join(outDir, "coreToOdt_SaveFull") + + with zipfile.ZipFile(fullFile, mode="r") as theZip: + theZip.extract("META-INF/manifest.xml", extaxtTo) + theZip.extract("settings.xml", extaxtTo) + theZip.extract("content.xml", extaxtTo) + theZip.extract("meta.xml", extaxtTo) + theZip.extract("styles.xml", extaxtTo) + + maniOut = os.path.join(outDir, "coreToOdt_SaveFull", "META-INF", "manifest.xml") + settOut = os.path.join(outDir, "coreToOdt_SaveFull", "settings.xml") + contOut = os.path.join(outDir, "coreToOdt_SaveFull", "content.xml") + metaOut = os.path.join(outDir, "coreToOdt_SaveFull", "meta.xml") + stylOut = os.path.join(outDir, "coreToOdt_SaveFull", "styles.xml") + + def prettifyXml(inFile, outFile): + with open(outFile, mode="wb") as fileStream: + fileStream.write( + etree.tostring( + etree.parse(inFile), + pretty_print=True, + encoding="utf-8", + xml_declaration=True + ) + ) + + prettifyXml(maniOut, maniFile) + prettifyXml(settOut, settFile) + prettifyXml(contOut, contFile) + prettifyXml(metaOut, metaFile) + prettifyXml(stylOut, stylFile) + + assert cmpFiles(maniFile, maniComp) + assert cmpFiles(settFile, settComp) + assert cmpFiles(contFile, contComp) + assert cmpFiles(metaFile, metaComp, [4, 5]) + assert cmpFiles(stylFile, stylComp) + +# END Test testCoreToOdt_SaveFull + + +@pytest.mark.core +def testCoreToOdt_ODTParagraphStyle(): + """Test the ODTParagraphStyle class. + """ + parStyle = ODTParagraphStyle() + + # Set Attributes + # ============== + + # Display, Parent, Next Style + assert parStyle._mAttr["display-name"] == ["style", None] + assert parStyle._mAttr["parent-style-name"] == ["style", None] + assert parStyle._mAttr["next-style-name"] == ["style", None] + + parStyle.setDisplayName("Name") + parStyle.setParentStyleName("Name") + parStyle.setNextStyleName("Name") + + assert parStyle._mAttr["display-name"] == ["style", "Name"] + assert parStyle._mAttr["parent-style-name"] == ["style", "Name"] + assert parStyle._mAttr["next-style-name"] == ["style", "Name"] + + # Outline Level + assert parStyle._mAttr["default-outline-level"] == ["style", None] + parStyle.setOutlineLevel("0") + assert parStyle._mAttr["default-outline-level"] == ["style", None] + parStyle.setOutlineLevel("1") + assert parStyle._mAttr["default-outline-level"] == ["style", "1"] + parStyle.setOutlineLevel("2") + assert parStyle._mAttr["default-outline-level"] == ["style", "2"] + parStyle.setOutlineLevel("3") + assert parStyle._mAttr["default-outline-level"] == ["style", "3"] + parStyle.setOutlineLevel("4") + assert parStyle._mAttr["default-outline-level"] == ["style", "4"] + parStyle.setOutlineLevel("5") + assert parStyle._mAttr["default-outline-level"] == ["style", None] + + # Class + assert parStyle._mAttr["class"] == ["style", None] + parStyle.setClass("stuff") + assert parStyle._mAttr["class"] == ["style", None] + parStyle.setClass("text") + assert parStyle._mAttr["class"] == ["style", "text"] + parStyle.setClass("chapter") + assert parStyle._mAttr["class"] == ["style", "chapter"] + parStyle.setClass("stuff") + assert parStyle._mAttr["class"] == ["style", None] + + # Set Paragraph Style + # =================== + + # Margins & Line Height + assert parStyle._pAttr["margin-top"] == ["fo", None] + assert parStyle._pAttr["margin-bottom"] == ["fo", None] + assert parStyle._pAttr["margin-left"] == ["fo", None] + assert parStyle._pAttr["margin-right"] == ["fo", None] + assert parStyle._pAttr["line-height"] == ["fo", None] + + parStyle.setMarginTop("0.000cm") + parStyle.setMarginBottom("0.000cm") + parStyle.setMarginLeft("0.000cm") + parStyle.setMarginRight("0.000cm") + parStyle.setLineHeight("1.15") + + assert parStyle._pAttr["margin-top"] == ["fo", "0.000cm"] + assert parStyle._pAttr["margin-bottom"] == ["fo", "0.000cm"] + assert parStyle._pAttr["margin-left"] == ["fo", "0.000cm"] + assert parStyle._pAttr["margin-right"] == ["fo", "0.000cm"] + assert parStyle._pAttr["line-height"] == ["fo", "1.15"] + + # Text Alignment + assert parStyle._pAttr["text-align"] == ["fo", None] + parStyle.setTextAlign("stuff") + assert parStyle._pAttr["text-align"] == ["fo", None] + parStyle.setTextAlign("start") + assert parStyle._pAttr["text-align"] == ["fo", "start"] + parStyle.setTextAlign("center") + assert parStyle._pAttr["text-align"] == ["fo", "center"] + parStyle.setTextAlign("end") + assert parStyle._pAttr["text-align"] == ["fo", "end"] + parStyle.setTextAlign("justify") + assert parStyle._pAttr["text-align"] == ["fo", "justify"] + parStyle.setTextAlign("inside") + assert parStyle._pAttr["text-align"] == ["fo", "inside"] + parStyle.setTextAlign("outside") + assert parStyle._pAttr["text-align"] == ["fo", "outside"] + parStyle.setTextAlign("left") + assert parStyle._pAttr["text-align"] == ["fo", "left"] + parStyle.setTextAlign("right") + assert parStyle._pAttr["text-align"] == ["fo", "right"] + parStyle.setTextAlign("stuff") + assert parStyle._pAttr["text-align"] == ["fo", None] + + # Break Before + assert parStyle._pAttr["break-before"] == ["fo", None] + parStyle.setBreakBefore("stuff") + assert parStyle._pAttr["break-before"] == ["fo", None] + parStyle.setBreakBefore("auto") + assert parStyle._pAttr["break-before"] == ["fo", "auto"] + parStyle.setBreakBefore("column") + assert parStyle._pAttr["break-before"] == ["fo", "column"] + parStyle.setBreakBefore("page") + assert parStyle._pAttr["break-before"] == ["fo", "page"] + parStyle.setBreakBefore("even-page") + assert parStyle._pAttr["break-before"] == ["fo", "even-page"] + parStyle.setBreakBefore("odd-page") + assert parStyle._pAttr["break-before"] == ["fo", "odd-page"] + parStyle.setBreakBefore("inherit") + assert parStyle._pAttr["break-before"] == ["fo", "inherit"] + parStyle.setBreakBefore("stuff") + assert parStyle._pAttr["break-before"] == ["fo", None] + + # Break After + assert parStyle._pAttr["break-after"] == ["fo", None] + parStyle.setBreakAfter("stuff") + assert parStyle._pAttr["break-after"] == ["fo", None] + parStyle.setBreakAfter("auto") + assert parStyle._pAttr["break-after"] == ["fo", "auto"] + parStyle.setBreakAfter("column") + assert parStyle._pAttr["break-after"] == ["fo", "column"] + parStyle.setBreakAfter("page") + assert parStyle._pAttr["break-after"] == ["fo", "page"] + parStyle.setBreakAfter("even-page") + assert parStyle._pAttr["break-after"] == ["fo", "even-page"] + parStyle.setBreakAfter("odd-page") + assert parStyle._pAttr["break-after"] == ["fo", "odd-page"] + parStyle.setBreakAfter("inherit") + assert parStyle._pAttr["break-after"] == ["fo", "inherit"] + parStyle.setBreakAfter("stuff") + assert parStyle._pAttr["break-after"] == ["fo", None] + + # Text Attributes + # =============== + + # Font Name, Family and Size + assert parStyle._tAttr["font-name"] == ["style", None] + assert parStyle._tAttr["font-family"] == ["fo", None] + assert parStyle._tAttr["font-size"] == ["fo", None] + + parStyle.setFontName("Verdana") + parStyle.setFontFamily("Verdana") + parStyle.setFontSize("12pt") + + assert parStyle._tAttr["font-name"] == ["style", "Verdana"] + assert parStyle._tAttr["font-family"] == ["fo", "Verdana"] + assert parStyle._tAttr["font-size"] == ["fo", "12pt"] + + # Font Weight + assert parStyle._tAttr["font-weight"] == ["fo", None] + parStyle.setFontWeight("stuff") + assert parStyle._tAttr["font-weight"] == ["fo", None] + parStyle.setFontWeight("normal") + assert parStyle._tAttr["font-weight"] == ["fo", "normal"] + parStyle.setFontWeight("inherit") + assert parStyle._tAttr["font-weight"] == ["fo", "inherit"] + parStyle.setFontWeight("bold") + assert parStyle._tAttr["font-weight"] == ["fo", "bold"] + parStyle.setFontWeight("stuff") + assert parStyle._tAttr["font-weight"] == ["fo", None] + + # Colour & Opacity + assert parStyle._tAttr["color"] == ["fo", None] + assert parStyle._tAttr["opacity"] == ["loext", None] + + parStyle.setColor("#000000") + parStyle.setOpacity("1.00") + + assert parStyle._tAttr["color"] == ["fo", "#000000"] + assert parStyle._tAttr["opacity"] == ["loext", "1.00"] + + # Pack XML + # ======== + xStyle = etree.Element("test", nsmap={ + "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", + "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0", + "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", + }) + parStyle.packXML(xStyle, "test") + assert xmlToText(xStyle) == ( + '' + '' + '' + '' + '' + '' + ) + + # Changes + # ======= + + aStyle = ODTParagraphStyle() + oStyle = ODTParagraphStyle() + assert aStyle.checkNew(oStyle) is False + assert aStyle.getID() == oStyle.getID() + + aStyle.setDisplayName("Name1") + oStyle.setDisplayName("Name2") + assert aStyle.checkNew(oStyle) is True + assert aStyle.getID() != oStyle.getID() + + aStyle = ODTParagraphStyle() + oStyle = ODTParagraphStyle() + aStyle.setMarginTop("0.000cm") + oStyle.setMarginTop("1.000cm") + assert aStyle.checkNew(oStyle) is True + assert aStyle.getID() != oStyle.getID() + + aStyle = ODTParagraphStyle() + oStyle = ODTParagraphStyle() + aStyle.setColor("#000000") + oStyle.setColor("#111111") + assert aStyle.checkNew(oStyle) is True + assert aStyle.getID() != oStyle.getID() + +# END Test testCoreToOdt_ODTParagraphStyle + + +@pytest.mark.core +def testCoreToOdt_ODTTextStyle(): + """Test the ODTTextStyle class. + """ + txtStyle = ODTTextStyle() + + # Font Weight + assert txtStyle._tAttr["font-weight"] == ["fo", None] + txtStyle.setFontWeight("stuff") + assert txtStyle._tAttr["font-weight"] == ["fo", None] + txtStyle.setFontWeight("normal") + assert txtStyle._tAttr["font-weight"] == ["fo", "normal"] + txtStyle.setFontWeight("inherit") + assert txtStyle._tAttr["font-weight"] == ["fo", "inherit"] + txtStyle.setFontWeight("bold") + assert txtStyle._tAttr["font-weight"] == ["fo", "bold"] + txtStyle.setFontWeight("stuff") + assert txtStyle._tAttr["font-weight"] == ["fo", None] + + # Font Style + assert txtStyle._tAttr["font-style"] == ["fo", None] + txtStyle.setFontStyle("stuff") + assert txtStyle._tAttr["font-style"] == ["fo", None] + txtStyle.setFontStyle("normal") + assert txtStyle._tAttr["font-style"] == ["fo", "normal"] + txtStyle.setFontStyle("inherit") + assert txtStyle._tAttr["font-style"] == ["fo", "inherit"] + txtStyle.setFontStyle("italic") + assert txtStyle._tAttr["font-style"] == ["fo", "italic"] + txtStyle.setFontStyle("stuff") + assert txtStyle._tAttr["font-style"] == ["fo", None] + + # Line Through Style + assert txtStyle._tAttr["text-line-through-style"] == ["style", None] + txtStyle.setStrikeStyle("stuff") + assert txtStyle._tAttr["text-line-through-style"] == ["style", None] + txtStyle.setStrikeStyle("none") + assert txtStyle._tAttr["text-line-through-style"] == ["style", "none"] + txtStyle.setStrikeStyle("solid") + assert txtStyle._tAttr["text-line-through-style"] == ["style", "solid"] + txtStyle.setStrikeStyle("stuff") + assert txtStyle._tAttr["text-line-through-style"] == ["style", None] + + # Line Through Type + assert txtStyle._tAttr["text-line-through-type"] == ["style", None] + txtStyle.setStrikeType("stuff") + assert txtStyle._tAttr["text-line-through-type"] == ["style", None] + txtStyle.setStrikeType("none") + assert txtStyle._tAttr["text-line-through-type"] == ["style", "none"] + txtStyle.setStrikeType("single") + assert txtStyle._tAttr["text-line-through-type"] == ["style", "single"] + txtStyle.setStrikeType("double") + assert txtStyle._tAttr["text-line-through-type"] == ["style", "double"] + txtStyle.setStrikeType("stuff") + assert txtStyle._tAttr["text-line-through-type"] == ["style", None] + + # Pack XML + # ======== + txtStyle.setFontWeight("bold") + txtStyle.setFontStyle("italic") + txtStyle.setStrikeStyle("solid") + txtStyle.setStrikeType("single") + xStyle = etree.Element("test", nsmap={ + "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", + "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", + }) + txtStyle.packXML(xStyle, "test") + assert xmlToText(xStyle) == ( + '' + ) + +# END Test testCoreToOdt_ODTTextStyle + + +@pytest.mark.core +def testCoreToOdt_XMLParagraph(): + """Test XML encoding of paragraph. + """ + nsMap = { + "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", + } + + # Stage 1 : Text + # ============== + + xRoot = etree.Element("root", nsmap=nsMap) + xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"]) + xmlPar = XMLParagraph(xElem) + + # Plain Text + xmlPar.appendText("Hello World") + assert xmlToText(xRoot) == ( + '' + 'Hello World' + '' + ) + + # Text Span + xmlPar.appendSpan("spanned text", "T1") + assert xmlToText(xRoot) == ( + '' + 'Hello World' + 'spanned text' + '' + '' + ) + + # Tail Text + xmlPar.appendText("more text") + assert xmlToText(xRoot) == ( + '' + 'Hello World' + 'spanned text' + 'more text' + '' + ) + + assert xmlPar.checkError() == (0, "") + + # Stage 2 : Line Breaks + # ===================== + + xRoot = etree.Element("root", nsmap=nsMap) + xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"]) + xmlPar = XMLParagraph(xElem) + + # Plain Text w/Line Break + xmlPar.appendText("Hello\nWorld\n!!") + assert xmlToText(xRoot) == ( + '' + 'HelloWorld!!' + '' + ) + + # Text Span w/Line Break + xmlPar.appendSpan("spanned\ntext", "T1") + assert xmlToText(xRoot) == ( + '' + 'HelloWorld!!' + 'spannedtext' + '' + ) + + # Tail Text w/Line Break + xmlPar.appendText("more\ntext") + assert xmlToText(xRoot) == ( + '' + 'HelloWorld!!' + 'spannedtext' + 'moretext' + '' + ) + + assert xmlPar.checkError() == (0, "") + + # Stage 3 : Tabs + # ============== + + xRoot = etree.Element("root", nsmap=nsMap) + xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"]) + xmlPar = XMLParagraph(xElem) + + # Plain Text w/Line Break + xmlPar.appendText("Hello\tWorld\t!!") + assert xmlToText(xRoot) == ( + '' + 'HelloWorld!!' + '' + ) + + # Text Span w/Line Break + xmlPar.appendSpan("spanned\ttext", "T1") + assert xmlToText(xRoot) == ( + '' + 'HelloWorld!!' + 'spannedtext' + '' + ) + + # Tail Text w/Line Break + xmlPar.appendText("more\ttext") + assert xmlToText(xRoot) == ( + '' + 'HelloWorld!!' + 'spannedtext' + 'moretext' + '' + ) + + assert xmlPar.checkError() == (0, "") + + # Stage 4 : Spaces + # ================ + + xRoot = etree.Element("root", nsmap=nsMap) + xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"]) + xmlPar = XMLParagraph(xElem) + + # Plain Text w/Spaces + xmlPar.appendText("Hello World !!") + assert xmlToText(xRoot) == ( + '' + 'Hello World !!' + '' + ) + + # Text Span w/Spaces + xmlPar.appendSpan("spanned text", "T1") + assert xmlToText(xRoot) == ( + '' + 'Hello World !!' + 'spanned text' + '' + ) + + # Tail Text w/Spaces + xmlPar.appendText("more text") + assert xmlToText(xRoot) == ( + '' + 'Hello World !!' + 'spanned text' + 'more text' + '' + ) + + assert xmlPar.checkError() == (0, "") + + # Stage 5 : Lots of Spaces + # ======================== + + xRoot = etree.Element("root", nsmap=nsMap) + xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"]) + xmlPar = XMLParagraph(xElem) + + # Plain Text w/Many Spaces + xmlPar.appendText(" \t A \n B ") + assert xmlToText(xRoot) == ( + '' + ' A B ' + '' + ) + + # Text Span w/Many Spaces + xmlPar.appendSpan(" C \t D \n E ", "T1") + assert xmlToText(xRoot) == ( + '' + ' A B ' + ' C D ' + ' E ' + '' + ) + + assert xmlPar.checkError() == (0, "") + + # Check Error + # =========== + + xRoot = etree.Element("root", nsmap=nsMap) + xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"]) + xmlPar = XMLParagraph(xElem) + + xmlPar.appendText("A") + xmlPar._nState = 5 + xmlPar.appendText("B") + + assert xmlPar.checkError() == (1, "1 char(s) were not written: 'AB'") + +# END Test testCoreToOdt_XMLParagraph + + +@pytest.mark.core +def testCoreToOdt_MkTag(): + """Test the tag maker function. + """ + assert _mkTag("office", "text") == "{urn:oasis:names:tc:opendocument:xmlns:office:1.0}text" + assert _mkTag("style", "text") == "{urn:oasis:names:tc:opendocument:xmlns:style:1.0}text" + assert _mkTag("blabla", "text") == "text" + +# END Test testCoreToOdt_MkTag