Add DocX test coverage

This commit is contained in:
Veronica Berglyd Olsen
2024-10-21 16:59:43 +02:00
parent 148d46fcbc
commit 787e1a46d1
16 changed files with 2041 additions and 26 deletions
+6 -17
View File
@@ -109,7 +109,7 @@ S_HEAD2 = "Heading2"
S_HEAD3 = "Heading3"
S_HEAD4 = "Heading4"
S_SEP = "Separator"
S_META = "TextMeta"
S_META = "MetaText"
S_HEAD = "Header"
S_FNOTE = "FootnoteText"
@@ -423,7 +423,8 @@ class ToDocX(Tokenizer):
par.addContent(xNode)
xNode = None
par.addContent(self._textRunToXml(text[fStart:fPos], xFmt))
if temp := text[fStart:fPos]:
par.addContent(self._textRunToXml(temp, xFmt))
if fFmt == self.FMT_B_B:
xFmt |= X_BLD
@@ -647,7 +648,7 @@ class ToDocX(Tokenizer):
# Add Text Meta Style
styles.append(DocXParStyle(
name="Text Meta",
name="Meta Text",
styleId=S_META,
size=fSz,
basedOn=S_NORM,
@@ -905,15 +906,8 @@ class ToDocX(Tokenizer):
for i, par in enumerate(self._pars):
if i > 0 and par.pageBreakBefore:
prev = self._pars[i-1]
if prev.pageBreakAfter:
# We already have a break, so we inject a new paragraph instead
empty = DocXParagraph()
empty.setStyle(self._styles.get(S_NORM))
empty.setPageBreakAfter(True)
pars.append(empty)
else:
par.setPageBreakBefore(False)
prev.setPageBreakAfter(True)
par.setPageBreakBefore(False)
prev.setPageBreakAfter(True)
pars.append(par)
@@ -1037,11 +1031,6 @@ class DocXParagraph:
"""Has page break before."""
return self._breakBefore
@property
def pageBreakAfter(self) -> bool:
"""Has page break after."""
return self._breakAfter
##
# Setters
##
@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Types xmlns:ns0="http://schemas.openxmlformats.org/package/2006/content-types">
<ns0:Default Extension="xml" ContentType="application/xml" />
<ns0:Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<ns0:Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<ns0:Override PartName="/word/_rels/document.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<ns0:Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
<ns0:Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" />
<ns0:Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" />
<ns0:Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" />
<ns0:Override PartName="/word/header2.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" />
<ns0:Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" />
<ns0:Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" />
<ns0:Override PartName="/word/footnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" />
</ns0:Types>
@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Properties xmlns:ns0="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<ns0:TotalTime>36</ns0:TotalTime>
<ns0:Application>novelWriter/2.6a1</ns0:Application>
<ns0:Words>4029</ns0:Words>
<ns0:Characters>21251</ns0:Characters>
<ns0:CharactersWithSpaces>24914</ns0:CharactersWithSpaces>
<ns0:Paragraphs>42</ns0:Paragraphs>
</ns0:Properties>
@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2024-10-21T13:54:49</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2024-10-21T13:54:49</dcterms:modified>
<dc:creator>lipsum.com</dc:creator>
<dc:title>Lorem Ipsum</dc:title>
<dc:creator>lipsum.com</dc:creator>
<dc:language>en_GB</dc:language>
<cp:revision>45</cp:revision>
<cp:lastModifiedBy>lipsum.com</cp:lastModifiedBy>
</coreProperties>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Relationships xmlns:ns0="http://schemas.openxmlformats.org/package/2006/relationships">
<ns0:Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml" />
<ns0:Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml" />
<ns0:Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header2.xml" />
<ns0:Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml" />
<ns0:Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml" />
</ns0:Relationships>
@@ -0,0 +1,20 @@
<?xml version='1.0' encoding='utf-8'?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnote w:id="1">
<w:p>
<w:pPr>
<w:pStyle w:val="FootnoteText" />
</w:pPr>
<w:r>
<w:rPr>
<w:i />
</w:rPr>
<w:t>Lorem ipsum</w:t>
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin. (Source: Wikipedia)</w:t>
</w:r>
</w:p>
</w:footnote>
</w:footnotes>
@@ -0,0 +1,38 @@
<?xml version='1.0' encoding='utf-8'?>
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<w:pPr>
<w:pStyle w:val="Header" />
<w:jc w:val="right" />
<w:rPr />
</w:pPr>
<w:r>
<w:rPr />
<w:t xml:space="preserve">Page </w:t>
</w:r>
<w:r>
<w:rPr />
<w:fldChar w:fldCharType="begin" />
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> PAGE </w:t>
</w:r>
<w:r>
<w:rPr />
<w:fldChar w:fldCharType="separate" />
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve">2</w:t>
</w:r>
<w:r>
<w:rPr />
<w:fldChar w:fldCharType="end" />
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> - Lorem Ipsum (lipsum.com)</w:t>
</w:r>
</w:p>
</w:hdr>
@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<w:pPr>
<w:pStyle w:val="Header" />
<w:jc w:val="right" />
<w:rPr />
</w:pPr>
<w:r>
<w:rPr />
</w:r>
</w:p>
</w:hdr>
@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Relationships xmlns:ns0="http://schemas.openxmlformats.org/package/2006/relationships">
<ns0:Relationship Id="rId1" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml" />
<ns0:Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml" />
<ns0:Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml" />
</ns0:Relationships>
@@ -0,0 +1,19 @@
<?xml version='1.0' encoding='utf-8'?>
<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnotePr>
<w:numFmt w:val="decimal" />
</w:footnotePr>
<w:docVars>
<w:docVar w:name="ManuscriptTitleCount" w:val="11" />
<w:docVar w:name="ManuscriptParagraphCount" w:val="42" />
<w:docVar w:name="ManuscriptAllWords" w:val="4029" />
<w:docVar w:name="ManuscriptTextWords" w:val="3705" />
<w:docVar w:name="ManuscriptTitleWords" w:val="21" />
<w:docVar w:name="ManuscriptAllChars" w:val="27014" />
<w:docVar w:name="ManuscriptTextChars" w:val="24914" />
<w:docVar w:name="ManuscriptTitleChars" w:val="123" />
<w:docVar w:name="ManuscriptAllWordChars" w:val="23075" />
<w:docVar w:name="ManuscriptTextWordChars" w:val="21251" />
<w:docVar w:name="ManuscriptTitleWordChars" w:val="113" />
</w:docVars>
</w:settings>
@@ -0,0 +1,153 @@
<?xml version='1.0' encoding='utf-8'?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:docDefaults>
<w:rPrDefault>
<w:rPr>
<w:rFonts w:ascii="Source Sans Pro" w:hAnsi="Source Sans Pro" w:cs="Source Sans Pro" />
<w:sz w:val="24" />
<w:szCs w:val="24" />
<w:lang w:val="en_GB" />
</w:rPr>
</w:rPrDefault>
<w:pPrDefault>
<w:pPr>
<w:spacing w:line="276" />
</w:pPr>
</w:pPrDefault>
</w:docDefaults>
<w:style w:type="paragraph" w:styleId="Normal" w:default="1">
<w:name w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="139" w:line="276" />
<w:jc w:val="left" />
</w:pPr>
<w:rPr>
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Title">
<w:name w:val="Title" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="340" w:after="120" w:line="690" />
<w:outlineLvl w:val="0" />
</w:pPr>
<w:rPr>
<w:b />
<w:sz w:val="60" />
<w:szCs w:val="60" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="340" w:after="120" w:line="552" />
<w:outlineLvl w:val="0" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="2a6099" />
<w:sz w:val="48" />
<w:szCs w:val="48" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading2">
<w:name w:val="Heading 2" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="400" w:after="120" w:line="483" />
<w:outlineLvl w:val="1" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="2a6099" />
<w:sz w:val="42" />
<w:szCs w:val="42" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading3">
<w:name w:val="Heading 3" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="280" w:after="120" w:line="414" />
<w:outlineLvl w:val="1" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="444444" />
<w:sz w:val="36" />
<w:szCs w:val="36" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading4">
<w:name w:val="Heading 4" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="280" w:after="120" w:line="345" />
<w:outlineLvl w:val="1" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="444444" />
<w:sz w:val="30" />
<w:szCs w:val="30" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Separator">
<w:name w:val="Separator" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="280" w:after="280" w:line="276" />
<w:jc w:val="center" />
</w:pPr>
<w:rPr>
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="MetaText">
<w:name w:val="Meta Text" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="139" w:line="276" />
</w:pPr>
<w:rPr>
<w:color w:val="813709" />
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Header">
<w:name w:val="Header" />
<w:basedOn w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="0" w:line="240" />
<w:jc w:val="right" />
</w:pPr>
<w:rPr>
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="FootnoteText">
<w:name w:val="Footnote Text" />
<w:basedOn w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="90" w:line="220" />
<w:ind w:left="272" w:hanging="272" />
</w:pPr>
<w:rPr>
<w:sz w:val="19" />
<w:szCs w:val="19" />
</w:rPr>
</w:style>
</w:styles>
+35 -5
View File
@@ -33,11 +33,12 @@ from PyQt5.QtGui import QColor, QDesktopServices, QFontDatabase
from novelwriter.common import (
NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, compact, cssCol, describeFont,
elide, formatFileFilter, formatInt, formatTime, formatTimeStamp,
formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode,
makeFileNameSafe, minmax, numberToRoman, openExternalPath, readTextFile,
simplified, transferCase, uniqueCompact, xmlIndent, yesNo
elide, firstFloat, formatFileFilter, formatInt, formatTime,
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle,
isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag,
jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath,
readTextFile, simplified, transferCase, uniqueCompact, xmlIndent,
xmlSubElem, yesNo
)
from tests.mocked import causeOSError
@@ -291,6 +292,15 @@ def testBaseCommon_checkIntTuple():
assert checkIntTuple(5, (0, 1, 2), 3) == 3
@pytest.mark.base
def testBaseCommon_firstFloat():
"""Test the firstFloat function."""
assert firstFloat(None, 1.0) == 1.0
assert firstFloat(1.0, None) == 1.0
assert firstFloat(None, 1) == 0.0
assert firstFloat(None, "1.0") == 0.0
@pytest.mark.base
def testBaseCommon_formatTimeStamp():
"""Test the formatTimeStamp function."""
@@ -624,6 +634,26 @@ def testBaseCommon_xmlIndent():
assert data == "foobar"
@pytest.mark.base
def testBaseCommon_xmlSubElem():
"""Test the xmlSubElem function."""
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", None, attrib={"a": "b"})
) == b'<node a="b" />'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})
) == b'<node a="b">text</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})
) == b'<node a="b">42</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", 3.14, attrib={"a": "b"})
) == b'<node a="b">3.14</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", True, attrib={"a": "b"})
) == b'<node a="b">true</node>'
@pytest.mark.base
def testBaseCommon_readTextFile(monkeypatch, fncPath, ipsumText):
"""Test the readTextFile function."""
+566
View File
@@ -0,0 +1,566 @@
"""
novelWriter ToDocX Class Tester
=================================
This file is a part of novelWriter
Copyright 20182024, 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 <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
import zipfile
import pytest
from novelwriter.common import xmlIndent
from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.project import NWProject
from novelwriter.enum import nwBuildFmt
from novelwriter.formats.todocx import (
S_FNOTE, S_HEAD1, S_HEAD2, S_HEAD3, S_HEAD4, S_META, S_NORM, S_SEP,
S_TITLE, ToDocX, _mkTag, _wTag
)
from tests.tools import DOCX_IGNORE, cmpFiles
OOXML_SCM = "http://schemas.openxmlformats.org"
XML_NS = [
f' xmlns:r="{OOXML_SCM}/officeDocument/2006/relationships"',
f' xmlns:w="{OOXML_SCM}/wordprocessingml/2006/main"',
f' xmlns:cp="{OOXML_SCM}/package/2006/metadata/core-properties"',
' xmlns:dc="http://purl.org/dc/elements/1.1/"',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
' xmlns:xml="http://www.w3.org/XML/1998/namespace"',
' xmlns:dcterms="http://purl.org/dc/terms/"',
]
def xmlToText(xElem):
"""Get the text content of an XML element."""
rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for ns in XML_NS:
rTxt = rTxt.replace(ns, "")
return rTxt
@pytest.mark.core
def testFmtToDocX_ParagraphStyles(mockGUI):
"""Test formatting of paragraphs."""
project = NWProject()
doc = ToDocX(project)
doc.setSynopsis(True)
doc.setComments(True)
doc.setKeywords(True)
doc.initDocument()
# Normal Text
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Title
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TITLE, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_TITLE}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 1
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD1, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD1}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 2
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD2, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD2}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 3
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD3, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD3}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 4
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD4, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD4}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Separator
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SEP, 0, "* * *", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_SEP}" /></w:pPr><w:r><w:rPr />'
'<w:t>* * *</w:t></w:r></w:p></w:body>'
)
# Empty Paragraph
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SKIP, 0, "* * *", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr></w:p></w:body>'
)
# Synopsis
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SYNOPSIS, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Synopsis:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Short
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SHORT, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Short Description:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Comment
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_COMMENT, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Comment:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Tags and References (Single)
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_KEYWORD, 0, "tag: Stuff", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Tag:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Stuff</w:t></w:r>'
'</w:p></w:body>'
)
# Tags and References (Multiple)
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_KEYWORD, 0, "char: Jane, John", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Characters:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Jane, John</w:t></w:r>'
'</w:p></w:body>'
)
# Tags and References (Invalid)
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_KEYWORD, 0, "stuff: Stuff", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr></w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_ParagraphFormatting(mockGUI):
"""Test formatting of paragraphs."""
project = NWProject()
doc = ToDocX(project)
doc.setSynopsis(True)
doc.setComments(True)
doc.setKeywords(True)
doc.initDocument()
# Left Align
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_LEFT)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="left" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Right Align
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_RIGHT)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="right" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Center Align
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_CENTRE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="center" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Justify
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_JUSTIFY)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="both" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Page Break Before
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_PBB)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Page Break After
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_PBA)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'<w:r><w:br w:type="page" /></w:r>'
'</w:p></w:body>'
)
# Zero Margins
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_Z_TOPMRG | doc.A_Z_BTMMRG)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:spacing w:before="0" w:after="0" w:line="252" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Indent
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_IND_L | doc.A_IND_R)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:ind w:left="880" w:right="880" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# First Line Indent
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_IND_T)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:ind w:firstLine="308" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_TextFormatting(mockGUI):
"""Test formatting of text."""
project = NWProject()
doc = ToDocX(project)
doc.initDocument()
# Markdown
xTest = ET.Element(_wTag("body"))
doc._text = "Text **bold**, _italic_, ~~strike~~."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Text </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>bold</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
'<w:r><w:rPr><w:i /></w:rPr><w:t>italic</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t>strike</w:t></w:r>'
'<w:r><w:rPr /><w:t>.</w:t></w:r>'
'</w:p></w:body>'
)
# Nested Shortcode Text, Emphasis
xTest = ET.Element(_wTag("body"))
doc._text = "Some [s]nested [b]bold[/b] [u]and[/u] [i]italics[/i] text[/s] text."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve">nested </w:t></w:r>'
'<w:r><w:rPr><w:b /><w:strike /></w:rPr><w:t>bold</w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /><w:strike /></w:rPr><w:t>and</w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:i /><w:strike /></w:rPr><w:t>italics</w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve"> text</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> text.</w:t></w:r>'
'</w:p></w:body>'
)
# Shortcode Text, Super/Subscript
xTest = ET.Element(_wTag("body"))
doc._text = "Some super[sup]script[/sup] and sub[sub]script[/sub] text."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Some super</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr><w:t>script</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> and sub</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="subscript" /></w:rPr><w:t>script</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> text.</w:t></w:r>'
'</w:p></w:body>'
)
# Shortcode Text, Underline/Highlight
xTest = ET.Element(_wTag("body"))
doc._text = "Some [u]underlined and [m]highlighted[/m][/u] text."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /></w:rPr>'
'<w:t xml:space="preserve">underlined and </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /><w:shd w:fill="ffffa6" w:val="clear" /></w:rPr>'
'<w:t>highlighted</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> text.</w:t></w:r>'
'</w:p></w:body>'
)
# Hard Break
xTest = ET.Element(_wTag("body"))
doc._text = "Some text.\nNext line\n"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Some text.</w:t><w:br /><w:t>Next line</w:t></w:r>'
'</w:p></w:body>'
)
# Tab
xTest = ET.Element(_wTag("body"))
doc._text = "\tItem 1\tItem 2\n"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:tab /><w:t>Item 1</w:t><w:tab /><w:t>Item 2</w:t></w:r>'
'</w:p></w:body>'
)
# Tab in Format
xTest = ET.Element(_wTag("body"))
doc._text = "Some **bold\ttext**"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>bold</w:t><w:tab /><w:t>text</w:t></w:r>'
'</w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_Footnotes(mockGUI):
"""Test formatting of footnotes."""
project = NWProject()
doc = ToDocX(project)
doc.initDocument()
# Text
xTest = ET.Element(_wTag("body"))
doc._text = (
"Text with one[footnote:fa], **two**[footnote:fd], "
"or three[footnote:fb] footnotes.[footnote:fe]\n\n"
"%footnote.fa: Footnote text A.[footnote:fc]\n\n"
"%footnote.fc: This footnote is skipped.\n\n"
"%footnote.fd: Another footnote.\n\n"
"%footnote.fe: Again?\n\n"
)
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Text with one</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="1" /></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>two</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="2" /></w:r>'
'<w:r><w:rPr /><w:t>, or three</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> footnotes.</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="3" /></w:r>'
'</w:p></w:body>'
)
# Footnotes
doc._footnotesXml()
assert xmlToText(doc._files["footnotes.xml"].xml) == (
'<w:footnotes>'
f'<w:footnote w:id="1"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Footnote text A.</w:t></w:r></w:p></w:footnote>'
f'<w:footnote w:id="2"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Another footnote.</w:t></w:r></w:p></w:footnote>'
f'<w:footnote w:id="3"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Again?</w:t></w:r></w:p></w:footnote>'
'</w:footnotes>'
)
@pytest.mark.core
def testFmtToDocX_SaveDocument(mockGUI, prjLipsum, fncPath, tstPaths):
"""Test document output."""
project = NWProject()
project.openProject(prjLipsum)
pageHeader = f"Page {nwHeadFmt.ODT_PAGE} - {nwHeadFmt.ODT_PROJECT} ({nwHeadFmt.ODT_AUTHOR})"
build = BuildSettings()
build.setValue("filter.includeNovel", True)
build.setValue("filter.includeNotes", True)
build.setValue("filter.includeInactive", False)
build.setValue("text.includeSynopsis", True)
build.setValue("text.includeComments", True)
build.setValue("text.includeKeywords", True)
build.setValue("format.textFont", "Source Sans Pro,12")
build.setValue("format.firstLineIndent", True)
build.setValue("odt.pageHeader", pageHeader)
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
docPath = fncPath / "document.docx"
assert list(docBuild.iterBuildDocument(docPath, nwBuildFmt.DOCX)) == [
(0, True), (1, True), (2, True), (3, True), (4, True), (5, False),
(6, True), (7, True), (8, True), (9, False), (10, False), (11, True),
(12, True), (13, True), (14, True), (15, True), (16, True), (17, True),
(18, True), (19, True), (20, True),
]
assert docPath.exists()
assert zipfile.is_zipfile(docPath)
with zipfile.ZipFile(docPath, mode="r") as zipObj:
zipObj.extractall(fncPath / "extract")
def prettifyXml(inFile, outFile):
with open(outFile, mode="wb") as fStream:
xml = ET.parse(inFile)
xmlIndent(xml)
xml.write(fStream, encoding="utf-8", xml_declaration=True)
expected = [
fncPath / "extract" / "[Content_Types].xml",
fncPath / "extract" / "_rels" / ".rels",
fncPath / "extract" / "docProps" / "app.xml",
fncPath / "extract" / "docProps" / "core.xml",
fncPath / "extract" / "word" / "_rels" / "document.xml.rels",
fncPath / "extract" / "word" / "document.xml",
fncPath / "extract" / "word" / "footnotes.xml",
fncPath / "extract" / "word" / "header1.xml",
fncPath / "extract" / "word" / "header2.xml",
fncPath / "extract" / "word" / "settings.xml",
fncPath / "extract" / "word" / "styles.xml",
]
outDir = tstPaths.outDir / "fmtToDocX_SaveDocument"
outDir.mkdir()
for file in expected:
assert file.is_file()
name = file.name.replace("[", "").replace("]", "").lstrip(".")
outFile = outDir / name
refFile = tstPaths.refDir / f"fmtToDocX_SaveDocument_{name}"
prettifyXml(file, outFile)
assert cmpFiles(outFile, refFile, ignoreStart=DOCX_IGNORE)
@pytest.mark.core
def testFmtToDocX_MkTag():
"""Test the tag maker function."""
assert _mkTag("r", "id") == f"{{{OOXML_SCM}/officeDocument/2006/relationships}}id"
assert _mkTag("w", "t") == f"{{{OOXML_SCM}/wordprocessingml/2006/main}}t"
assert _mkTag("q", "t") == "t"
+3 -3
View File
@@ -1,6 +1,6 @@
"""
novelWriter ToOdt Class Tester
=================================
================================
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
@@ -48,8 +48,8 @@ XML_NS = [
def xmlToText(xElem):
"""Get the text content of an XML element."""
rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for nSpace in XML_NS:
rTxt = rTxt.replace(nSpace, "")
for ns in XML_NS:
rTxt = rTxt.replace(ns, "")
return rTxt
+3 -1
View File
@@ -30,6 +30,7 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QWidget
XML_IGNORE = ("<novelWriterXML", "<project")
ODT_IGNORE = ("<meta:generator", "<meta:creation-date", "<dc:date", "<meta:editing")
NWD_IGNORE = ("%%~date:",)
DOCX_IGNORE = ("<dcterms:created", "<dcterms:modified")
MOCK_TIME = datetime(2019, 5, 10, 18, 52, 0).timestamp()
@@ -59,7 +60,8 @@ class C:
def cmpFiles(
fileOne: str | Path, fileTwo: str | Path,
fileOne: str | Path,
fileTwo: str | Path,
ignoreLines: list | None = None,
ignoreStart: tuple | None = None
) -> bool: