diff --git a/novelwriter/common.py b/novelwriter/common.py
index 397f6dc5..bf150794 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -492,6 +492,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
return "".join(buffer)
+##
+# XML Helpers
+##
+
def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
"""A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does.
@@ -535,21 +539,42 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
return
+def xmlElement(
+ tag: str,
+ text: str | int | float | bool | None = None,
+ *,
+ attrib: dict | None = None,
+ tail: str | None = None,
+) -> ET.Element:
+ """A custom implementation of Element with more arguments."""
+ xSub = ET.Element(tag, attrib=attrib or {})
+ if text is not None:
+ if isinstance(text, bool):
+ xSub.text = str(text).lower()
+ else:
+ xSub.text = str(text)
+ if tail is not None:
+ xSub.tail = tail
+ return xSub
+
+
def xmlSubElem(
parent: ET.Element,
tag: str,
text: str | int | float | bool | None = None,
- attrib: dict | None = None
+ *,
+ attrib: dict | None = None,
+ tail: str | None = None,
) -> ET.Element:
- """A custom implementation of SubElement that takes text as an
- argument.
- """
+ """A custom implementation of SubElement with more arguments."""
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
if text is not None:
if isinstance(text, bool):
xSub.text = str(text).lower()
else:
xSub.text = str(text)
+ if tail is not None:
+ xSub.tail = tail
return xSub
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index af232328..954f21ed 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -334,12 +334,12 @@ class nwLabels:
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
- nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)"),
+ nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "HTML 5 (.html)"),
nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
- nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)"),
+ nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"),
nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
}
BUILD_EXT = {
diff --git a/novelwriter/enum.py b/novelwriter/enum.py
index f59a7047..b6135344 100644
--- a/novelwriter/enum.py
+++ b/novelwriter/enum.py
@@ -184,11 +184,11 @@ class nwBuildFmt(Enum):
ODT = 0
FODT = 1
DOCX = 2
- HTML = 3
- NWD = 4
+ PDF = 3
+ HTML = 4
STD_MD = 5
EXT_MD = 6
- PDF = 7
+ NWD = 7
J_HTML = 8
J_NWD = 9
diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py
index 6461cfe9..82ed2e36 100644
--- a/novelwriter/formats/todocx.py
+++ b/novelwriter/formats/todocx.py
@@ -37,7 +37,7 @@ from PyQt5.QtCore import QMarginsF, QSizeF
from PyQt5.QtGui import QColor
from novelwriter import __version__
-from novelwriter.common import firstFloat, xmlSubElem
+from novelwriter.common import firstFloat, xmlElement, xmlSubElem
from novelwriter.constants import nwHeadFmt, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
@@ -57,13 +57,13 @@ RELS_BASE = f"{OOXML_SCM}/officeDocument/2006/relationships"
# Main XML NameSpaces
XML_NS = {
- "r": RELS_BASE,
- "w": f"{OOXML_SCM}/wordprocessingml/2006/main",
"cp": f"{OOXML_SCM}/package/2006/metadata/core-properties",
"dc": "http://purl.org/dc/elements/1.1/",
- "xsi": "http://www.w3.org/2001/XMLSchema-instance",
- "xml": "http://www.w3.org/XML/1998/namespace",
"dcterms": "http://purl.org/dc/terms/",
+ "r": RELS_BASE,
+ "w": f"{OOXML_SCM}/wordprocessingml/2006/main",
+ "xml": "http://www.w3.org/XML/1998/namespace",
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
for ns, uri in XML_NS.items():
ET.register_namespace(ns, uri)
@@ -328,10 +328,10 @@ class ToDocX(Tokenizer):
cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE))
# Relationships XML
- rRels = ET.Element("Relationships", attrib={
+ rRels = xmlElement("Relationships", attrib={
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
})
- wRels = ET.Element("Relationships", attrib={
+ wRels = xmlElement("Relationships", attrib={
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
})
for name, rel in self._rels.items():
@@ -347,7 +347,7 @@ class ToDocX(Tokenizer):
xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib)
# Content Types XML
- dTypes = ET.Element("Types", attrib={
+ dTypes = xmlElement("Types", attrib={
"xmlns": f"{OOXML_SCM}/package/2006/content-types"
})
for name, content in cExts:
@@ -355,10 +355,8 @@ class ToDocX(Tokenizer):
for name, content in cDocs:
xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content})
- def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
- with zipObj.open(name, mode="w") as fObj:
- xml = ET.ElementTree(xObj)
- xml.write(fObj, encoding="utf-8", xml_declaration=True)
+ def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
+ zipObj.writestr(name, ET.tostring(root, encoding="utf-8", xml_declaration=True))
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
xmlToZip("_rels/.rels", rRels, outZip)
@@ -435,7 +433,7 @@ class ToDocX(Tokenizer):
elif fFmt == TextFmt.FNOTE:
xNode = self._generateFootnote(fData)
elif fFmt == TextFmt.FIELD:
- xNode = self._generateField(fData)
+ xNode = self._generateField(fData, xFmt)
elif fFmt == TextFmt.STRIP:
pass
@@ -450,9 +448,9 @@ class ToDocX(Tokenizer):
return
- def _textRunToXml(self, text: str, fmt: int, fClass: str, fLink: str) -> ET.Element:
+ def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
"""Encode the text run into XML."""
- xR = ET.Element(_wTag("r"))
+ xR = xmlElement(_wTag("r"))
rPr = xmlSubElem(xR, _wTag("rPr"))
if fmt & X_BLD:
xmlSubElem(rPr, _wTag("b"))
@@ -473,18 +471,20 @@ class ToDocX(Tokenizer):
if fmt & X_COL and (color := self._classes.get(fClass)):
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
- for segment in RX_TEXT.split(text):
- if segment == "\n":
- xmlSubElem(xR, _wTag("br"))
- elif segment == "\t":
- xmlSubElem(xR, _wTag("tab"))
- elif segment:
- _wText(xR, segment)
+ if isinstance(text, str):
+ for segment in RX_TEXT.split(text):
+ if segment == "\n":
+ xmlSubElem(xR, _wTag("br"))
+ elif segment == "\t":
+ xmlSubElem(xR, _wTag("tab"))
+ elif segment:
+ _wText(xR, segment)
if fmt & X_HRF and fLink:
xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
- rId = self._appendExternalRel(fLink)
- xH = ET.Element(_wTag("hyperlink"), attrib={_mkTag("r", "id"): rId})
+ xH = xmlElement(_wTag("hyperlink"), attrib={
+ _mkTag("r", "id"): self._appendExternalRel(fLink),
+ })
xH.append(xR)
return xH
@@ -498,7 +498,7 @@ class ToDocX(Tokenizer):
"""Generate a footnote XML object."""
if key in self._footnotes:
idx = len(self._usedNotes) + 1
- xR = ET.Element(_wTag("r"))
+ xR = xmlElement(_wTag("r"))
rPr = xmlSubElem(xR, _wTag("rPr"))
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
@@ -506,11 +506,11 @@ class ToDocX(Tokenizer):
return xR
return None
- def _generateField(self, key: str) -> ET.Element | None:
+ def _generateField(self, key: str, fmt: int) -> ET.Element | None:
"""Generate a data field XML object."""
if key and (field := key.partition(":")[2]):
- xR = ET.Element(_wTag("r"))
- xT = xmlSubElem(xR, _wTag("t"), "0")
+ xR = self._textRunToXml(None, fmt, "", "")
+ xT = _wText(xR, "0")
self._usedFields.append((xT, field))
return xR
return None
@@ -687,7 +687,7 @@ class ToDocX(Tokenizer):
def _appXml(self) -> str:
"""Populate app.xml."""
rId = self._nextRelId()
- xRoot = ET.Element("Properties", attrib={
+ xRoot = xmlElement("Properties", attrib={
"xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties"
})
self._rels["app.xml"] = DocXXmlRel(
@@ -716,7 +716,7 @@ class ToDocX(Tokenizer):
def _coreXml(self) -> str:
"""Populate app.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_mkTag("cp", "coreProperties"))
+ xRoot = xmlElement(_mkTag("cp", "coreProperties"))
self._rels["core.xml"] = DocXXmlRel(
rId=rId,
relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
@@ -742,7 +742,7 @@ class ToDocX(Tokenizer):
def _stylesXml(self) -> str:
"""Populate styles.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("styles"))
+ xRoot = xmlElement(_wTag("styles"))
self._rels["styles.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/styles",
@@ -830,7 +830,7 @@ class ToDocX(Tokenizer):
def _defaultHeaderXml(self) -> str:
"""Populate header1.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("hdr"))
+ xRoot = xmlElement(_wTag("hdr"))
self._rels["header1.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/header",
@@ -877,7 +877,7 @@ class ToDocX(Tokenizer):
def _firstHeaderXml(self) -> str:
"""Populate header2.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("hdr"))
+ xRoot = xmlElement(_wTag("hdr"))
self._rels["header2.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/header",
@@ -902,7 +902,7 @@ class ToDocX(Tokenizer):
def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str:
"""Populate document.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("document"))
+ xRoot = xmlElement(_wTag("document"))
xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml")
xBody = xmlSubElem(xRoot, _wTag("body"))
self._rels["document.xml"] = DocXXmlRel(
@@ -978,7 +978,7 @@ class ToDocX(Tokenizer):
def _footnotesXml(self) -> str:
"""Populate footnotes.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("footnotes"))
+ xRoot = xmlElement(_wTag("footnotes"))
self._rels["footnotes.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/footnotes",
@@ -1001,7 +1001,7 @@ class ToDocX(Tokenizer):
def _fontTableXml(self) -> str:
"""Populate fontTable.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("fonts"))
+ xRoot = xmlElement(_wTag("fonts"))
self._rels["fontTable.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/fontTable",
@@ -1024,7 +1024,7 @@ class ToDocX(Tokenizer):
def _settingsXml(self) -> str:
"""Populate settings.xml."""
rId = self._nextRelId()
- xRoot = ET.Element(_wTag("settings"))
+ xRoot = xmlElement(_wTag("settings"))
self._rels["settings.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/settings",
diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py
index b3286170..f72aa6a4 100644
--- a/novelwriter/formats/toodt.py
+++ b/novelwriter/formats/toodt.py
@@ -38,7 +38,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
from PyQt5.QtGui import QColor, QFont
from novelwriter import __version__
-from novelwriter.common import xmlIndent, xmlSubElem
+from novelwriter.common import xmlElement, xmlIndent, xmlSubElem
from novelwriter.constants import nwHeadFmt, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape
@@ -54,6 +54,7 @@ XML_NS = {
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
+ "number": "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
"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",
@@ -115,6 +116,7 @@ S_FIND = "First_20_line_20_indent"
S_TEXT = "Text_20_body"
S_META = "Text_20_Meta"
S_HNF = "Header_20_and_20_Footer"
+S_NUM = "N0"
# Font Data
FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
@@ -513,13 +515,14 @@ class ToOdt(Tokenizer):
oVers = _mkTag("office", "version")
xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
- def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
- with zipObj.open(name, mode="w") as fObj:
- xml = ET.ElementTree(xObj)
- xml.write(fObj, encoding="utf-8", xml_declaration=True)
+ def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
+ zipObj.writestr(
+ name, ET.tostring(root, encoding="utf-8", xml_declaration=True),
+ compress_type=ZIP_DEFLATED, compresslevel=3,
+ )
- with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
- outZip.writestr("mimetype", X_MIME)
+ with ZipFile(path, mode="w") as outZip:
+ outZip.writestr("mimetype", X_MIME, compress_type=None, compresslevel=None)
xmlToZip("META-INF/manifest.xml", xMani, outZip)
xmlToZip("settings.xml", xSett, outZip)
xmlToZip("content.xml", self._dCont, outZip)
@@ -627,7 +630,7 @@ class ToOdt(Tokenizer):
elif fFmt == TextFmt.FNOTE:
xNode = self._generateFootnote(fData)
elif fFmt == TextFmt.FIELD:
- xNode = self._generateField(fData)
+ xNode = self._generateField(fData, xFmt)
elif fFmt == TextFmt.STRIP:
pass
@@ -713,24 +716,31 @@ class ToOdt(Tokenizer):
if content := self._footnotes.get(key):
self._nNote += 1
nStyle = ODTParagraphStyle("New")
- xNote = ET.Element(_mkTag("text", "note"), attrib={
+ xNote = xmlElement(_mkTag("text", "note"), attrib={
_mkTag("text", "id"): f"ftn{self._nNote}",
_mkTag("text", "note-class"): "footnote",
})
xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote)
- xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
+ xBody = xmlSubElem(xNote, _mkTag("text", "note-body"))
self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
return xNote
return None
- def _generateField(self, key: str) -> ET.Element | None:
+ def _generateField(self, key: str, fmt: int) -> ET.Element | None:
"""Generate a data field XML object."""
if key and (field := key.partition(":")[2]):
- xField = ET.Element(_mkTag("text", "user-field-get"), attrib={
+ xField = xmlElement(_mkTag("text", "user-field-get"), "0", tail="", attrib={
+ _mkTag("style", "data-style-name"): S_NUM,
_mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}",
})
- xField.text = "0"
- return xField
+ if fmt == 0x00:
+ return xField
+ else:
+ xSpan = xmlElement(TAG_SPAN, "", tail="", attrib={
+ _mkTag("text", "style-name"): self._textStyle(fmt),
+ })
+ xSpan.append(xField)
+ return xSpan
return None
def _emToCm(self, value: float) -> str:
@@ -836,6 +846,14 @@ class ToOdt(Tokenizer):
_mkTag("style", "class"): "extra",
})
+ # Numbers Style
+ xStyl = ET.SubElement(self._xStyl, _mkTag("number", "number-style"), attrib={
+ _mkTag("style", "name"): S_NUM,
+ })
+ ET.SubElement(xStyl, _mkTag("number", "number"), attrib={
+ _mkTag("number", "min-integer-digits"): "1",
+ })
+
return
def _useableStyles(self) -> None:
diff --git a/tests/reference/fmtToDocX_SaveDocument_document.xml b/tests/reference/fmtToDocX_SaveDocument_document.xml
index 4d840bc5..132468c6 100644
--- a/tests/reference/fmtToDocX_SaveDocument_document.xml
+++ b/tests/reference/fmtToDocX_SaveDocument_document.xml
@@ -33,6 +33,7 @@
Word Count:
+
4,035
@@ -46,6 +47,7 @@
Character Count:
+
27,064
diff --git a/tests/reference/fmtToOdt_SaveFlat_document.fodt b/tests/reference/fmtToOdt_SaveFlat_document.fodt
index 508c8cd9..1e82c1c6 100644
--- a/tests/reference/fmtToOdt_SaveFlat_document.fodt
+++ b/tests/reference/fmtToOdt_SaveFlat_document.fodt
@@ -1,13 +1,13 @@
-
+
- 2024-10-28T20:17:34
+ 2024-10-29T09:33:22
novelWriter/2.6a3
Jane Smith
1234
P42DT12H34M56S
Test Project
- 2024-10-28T20:17:34
+ 2024-10-29T09:33:22
Jane Smith
@@ -26,6 +26,9 @@
+
+
+
diff --git a/tests/reference/fmtToOdt_SaveFull_styles.xml b/tests/reference/fmtToOdt_SaveFull_styles.xml
index aa1f8a94..9b34773a 100644
--- a/tests/reference/fmtToOdt_SaveFull_styles.xml
+++ b/tests/reference/fmtToOdt_SaveFull_styles.xml
@@ -1,5 +1,5 @@
-
+
@@ -16,6 +16,9 @@
+
+
+
diff --git a/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt b/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt
index 6299ba17..9f61c7a4 100644
--- a/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt
+++ b/tests/reference/mBuildDocBuild_OpenDocument_Lorem_Ipsum.fodt
@@ -1,13 +1,13 @@
-
+
- 2024-10-28T18:27:00
+ 2024-10-29T09:41:30
novelWriter/2.6a3
lipsum.com
50
P0DT0H40M48S
Lorem Ipsum
- 2024-10-28T18:27:00
+ 2024-10-29T09:41:30
lipsum.com
@@ -26,6 +26,9 @@
+
+
+
@@ -164,8 +167,8 @@
Lorem Ipsum
By lipsum.com
- Word Count: 0
- Character Count: 0
+ Word Count: 0
+ Character Count: 0
“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_base/test_base_common.py b/tests/test_base/test_base_common.py
index 9c1f8dff..8eae1ba0 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -37,8 +37,8 @@ from novelwriter.common import (
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle,
isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag,
jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath,
- readTextFile, simplified, transferCase, uniqueCompact, xmlIndent,
- xmlSubElem, yesNo
+ readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
+ xmlIndent, xmlSubElem, yesNo
)
from tests.mocked import causeOSError
@@ -634,6 +634,29 @@ def testBaseCommon_xmlIndent():
assert data == "foobar"
+@pytest.mark.base
+def testBaseCommon_xmlElement():
+ """Test the xmlElement function."""
+ assert ET.tostring(
+ xmlElement("node", None, attrib={"a": "b"})
+ ) == b''
+ assert ET.tostring(
+ xmlElement("node", "text", attrib={"a": "b"})
+ ) == b'text'
+ assert ET.tostring(
+ xmlElement("node", "text", tail="foo", attrib={"a": "b"})
+ ) == b'textfoo'
+ assert ET.tostring(
+ xmlElement("node", 42, attrib={"a": "b"})
+ ) == b'42'
+ assert ET.tostring(
+ xmlElement("node", 3.14, attrib={"a": "b"})
+ ) == b'3.14'
+ assert ET.tostring(
+ xmlElement("node", True, attrib={"a": "b"})
+ ) == b'true'
+
+
@pytest.mark.base
def testBaseCommon_xmlSubElem():
"""Test the xmlSubElem function."""
@@ -643,6 +666,9 @@ def testBaseCommon_xmlSubElem():
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})
) == b'text'
+ assert ET.tostring(
+ xmlSubElem(ET.Element("r"), "node", "text", tail="foo", attrib={"a": "b"})
+ ) == b'textfoo'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})
) == b'42'
diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py
index 713aab50..04ec4e75 100644
--- a/tests/test_core/test_core_docbuild.py
+++ b/tests/test_core/test_core_docbuild.py
@@ -321,7 +321,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
@pytest.mark.core
-def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath):
+def testCoreDocBuild_DocX(mockGUI, prjLipsum, fncPath):
"""Test building a Word manuscript."""
project = NWProject()
project.openProject(prjLipsum)
@@ -352,22 +352,41 @@ def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath):
assert docFile.is_file()
assert zipfile.is_zipfile(docFile)
- # Check Error Handling
- # ====================
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
+@pytest.mark.core
+def testCoreDocBuild_PDF(mockGUI, prjLipsum, fncPath):
+ """Test building a PDF manuscript."""
+ project = NWProject()
+ project.openProject(prjLipsum)
- docFile = fncPath / "Lorem Ipsum Err.fodt"
- for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.FODT):
- pass
+ build = BuildSettings()
+ build.unpack(BUILD_CONF)
- assert docBuild.error == "OSError: Mock OSError"
- assert not docFile.is_file()
+ docBuild = NWBuildDocument(project, build)
+ docBuild.queueAll()
+
+ assert len(docBuild) == 21
+
+ # Check Build
+ # ===========
+
+ docFile = fncPath / "Lorem Ipsum.pdf"
+
+ count = 0
+ error = []
+ for _, success in docBuild.iterBuildDocument(docFile, nwBuildFmt.PDF):
+ count += 1 if success else 0
+ if docBuild.error:
+ error.append(docBuild.error)
+
+ assert count == 19
+ assert error == []
+
+ assert docFile.is_file()
@pytest.mark.core
-def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
+def testCoreDocBuild_NWD(mockGUI, prjLipsum, fncPath, tstPaths):
"""Test building a NWD manuscript."""
project = NWProject()
project.openProject(prjLipsum)
@@ -420,19 +439,6 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
- # Check Error Handling
- # ====================
-
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
-
- docFile = fncPath / "Lorem Ipsum Err.md"
- for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.NWD):
- pass
-
- assert docBuild.error == "OSError: Mock OSError"
- assert not docFile.is_file()
-
@pytest.mark.core
def testCoreDocBuild_Custom(mockGUI, fncPath: Path):