Fix variable formatting for DocX and ODT
This commit is contained in:
+29
-4
@@ -492,6 +492,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
|
|||||||
return "".join(buffer)
|
return "".join(buffer)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# XML Helpers
|
||||||
|
##
|
||||||
|
|
||||||
def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
|
def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
|
||||||
"""A modified version of the XML indent function in the standard
|
"""A modified version of the XML indent function in the standard
|
||||||
library. It behaves more closely to how the one from lxml does.
|
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
|
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(
|
def xmlSubElem(
|
||||||
parent: ET.Element,
|
parent: ET.Element,
|
||||||
tag: str,
|
tag: str,
|
||||||
text: str | int | float | bool | None = None,
|
text: str | int | float | bool | None = None,
|
||||||
attrib: dict | None = None
|
*,
|
||||||
|
attrib: dict | None = None,
|
||||||
|
tail: str | None = None,
|
||||||
) -> ET.Element:
|
) -> ET.Element:
|
||||||
"""A custom implementation of SubElement that takes text as an
|
"""A custom implementation of SubElement with more arguments."""
|
||||||
argument.
|
|
||||||
"""
|
|
||||||
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
|
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
|
||||||
if text is not None:
|
if text is not None:
|
||||||
if isinstance(text, bool):
|
if isinstance(text, bool):
|
||||||
xSub.text = str(text).lower()
|
xSub.text = str(text).lower()
|
||||||
else:
|
else:
|
||||||
xSub.text = str(text)
|
xSub.text = str(text)
|
||||||
|
if tail is not None:
|
||||||
|
xSub.tail = tail
|
||||||
return xSub
|
return xSub
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -334,12 +334,12 @@ class nwLabels:
|
|||||||
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
|
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
|
||||||
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
|
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
|
||||||
nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
|
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.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
|
||||||
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
|
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
|
||||||
nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
|
nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
|
||||||
nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
|
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)"),
|
nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
|
||||||
}
|
}
|
||||||
BUILD_EXT = {
|
BUILD_EXT = {
|
||||||
|
|||||||
+3
-3
@@ -184,11 +184,11 @@ class nwBuildFmt(Enum):
|
|||||||
ODT = 0
|
ODT = 0
|
||||||
FODT = 1
|
FODT = 1
|
||||||
DOCX = 2
|
DOCX = 2
|
||||||
HTML = 3
|
PDF = 3
|
||||||
NWD = 4
|
HTML = 4
|
||||||
STD_MD = 5
|
STD_MD = 5
|
||||||
EXT_MD = 6
|
EXT_MD = 6
|
||||||
PDF = 7
|
NWD = 7
|
||||||
J_HTML = 8
|
J_HTML = 8
|
||||||
J_NWD = 9
|
J_NWD = 9
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from PyQt5.QtCore import QMarginsF, QSizeF
|
|||||||
from PyQt5.QtGui import QColor
|
from PyQt5.QtGui import QColor
|
||||||
|
|
||||||
from novelwriter import __version__
|
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.constants import nwHeadFmt, nwStyles
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
|
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
|
# Main XML NameSpaces
|
||||||
XML_NS = {
|
XML_NS = {
|
||||||
"r": RELS_BASE,
|
|
||||||
"w": f"{OOXML_SCM}/wordprocessingml/2006/main",
|
|
||||||
"cp": f"{OOXML_SCM}/package/2006/metadata/core-properties",
|
"cp": f"{OOXML_SCM}/package/2006/metadata/core-properties",
|
||||||
"dc": "http://purl.org/dc/elements/1.1/",
|
"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/",
|
"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():
|
for ns, uri in XML_NS.items():
|
||||||
ET.register_namespace(ns, uri)
|
ET.register_namespace(ns, uri)
|
||||||
@@ -328,10 +328,10 @@ class ToDocX(Tokenizer):
|
|||||||
cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE))
|
cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE))
|
||||||
|
|
||||||
# Relationships XML
|
# Relationships XML
|
||||||
rRels = ET.Element("Relationships", attrib={
|
rRels = xmlElement("Relationships", attrib={
|
||||||
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
|
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
|
||||||
})
|
})
|
||||||
wRels = ET.Element("Relationships", attrib={
|
wRels = xmlElement("Relationships", attrib={
|
||||||
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
|
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
|
||||||
})
|
})
|
||||||
for name, rel in self._rels.items():
|
for name, rel in self._rels.items():
|
||||||
@@ -347,7 +347,7 @@ class ToDocX(Tokenizer):
|
|||||||
xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib)
|
xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib)
|
||||||
|
|
||||||
# Content Types XML
|
# Content Types XML
|
||||||
dTypes = ET.Element("Types", attrib={
|
dTypes = xmlElement("Types", attrib={
|
||||||
"xmlns": f"{OOXML_SCM}/package/2006/content-types"
|
"xmlns": f"{OOXML_SCM}/package/2006/content-types"
|
||||||
})
|
})
|
||||||
for name, content in cExts:
|
for name, content in cExts:
|
||||||
@@ -355,10 +355,8 @@ class ToDocX(Tokenizer):
|
|||||||
for name, content in cDocs:
|
for name, content in cDocs:
|
||||||
xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content})
|
xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content})
|
||||||
|
|
||||||
def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
|
def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
|
||||||
with zipObj.open(name, mode="w") as fObj:
|
zipObj.writestr(name, ET.tostring(root, encoding="utf-8", xml_declaration=True))
|
||||||
xml = ET.ElementTree(xObj)
|
|
||||||
xml.write(fObj, encoding="utf-8", xml_declaration=True)
|
|
||||||
|
|
||||||
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
|
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
|
||||||
xmlToZip("_rels/.rels", rRels, outZip)
|
xmlToZip("_rels/.rels", rRels, outZip)
|
||||||
@@ -435,7 +433,7 @@ class ToDocX(Tokenizer):
|
|||||||
elif fFmt == TextFmt.FNOTE:
|
elif fFmt == TextFmt.FNOTE:
|
||||||
xNode = self._generateFootnote(fData)
|
xNode = self._generateFootnote(fData)
|
||||||
elif fFmt == TextFmt.FIELD:
|
elif fFmt == TextFmt.FIELD:
|
||||||
xNode = self._generateField(fData)
|
xNode = self._generateField(fData, xFmt)
|
||||||
elif fFmt == TextFmt.STRIP:
|
elif fFmt == TextFmt.STRIP:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -450,9 +448,9 @@ class ToDocX(Tokenizer):
|
|||||||
|
|
||||||
return
|
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."""
|
"""Encode the text run into XML."""
|
||||||
xR = ET.Element(_wTag("r"))
|
xR = xmlElement(_wTag("r"))
|
||||||
rPr = xmlSubElem(xR, _wTag("rPr"))
|
rPr = xmlSubElem(xR, _wTag("rPr"))
|
||||||
if fmt & X_BLD:
|
if fmt & X_BLD:
|
||||||
xmlSubElem(rPr, _wTag("b"))
|
xmlSubElem(rPr, _wTag("b"))
|
||||||
@@ -473,18 +471,20 @@ class ToDocX(Tokenizer):
|
|||||||
if fmt & X_COL and (color := self._classes.get(fClass)):
|
if fmt & X_COL and (color := self._classes.get(fClass)):
|
||||||
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
|
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
|
||||||
|
|
||||||
for segment in RX_TEXT.split(text):
|
if isinstance(text, str):
|
||||||
if segment == "\n":
|
for segment in RX_TEXT.split(text):
|
||||||
xmlSubElem(xR, _wTag("br"))
|
if segment == "\n":
|
||||||
elif segment == "\t":
|
xmlSubElem(xR, _wTag("br"))
|
||||||
xmlSubElem(xR, _wTag("tab"))
|
elif segment == "\t":
|
||||||
elif segment:
|
xmlSubElem(xR, _wTag("tab"))
|
||||||
_wText(xR, segment)
|
elif segment:
|
||||||
|
_wText(xR, segment)
|
||||||
|
|
||||||
if fmt & X_HRF and fLink:
|
if fmt & X_HRF and fLink:
|
||||||
xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
|
xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
|
||||||
rId = self._appendExternalRel(fLink)
|
xH = xmlElement(_wTag("hyperlink"), attrib={
|
||||||
xH = ET.Element(_wTag("hyperlink"), attrib={_mkTag("r", "id"): rId})
|
_mkTag("r", "id"): self._appendExternalRel(fLink),
|
||||||
|
})
|
||||||
xH.append(xR)
|
xH.append(xR)
|
||||||
return xH
|
return xH
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ class ToDocX(Tokenizer):
|
|||||||
"""Generate a footnote XML object."""
|
"""Generate a footnote XML object."""
|
||||||
if key in self._footnotes:
|
if key in self._footnotes:
|
||||||
idx = len(self._usedNotes) + 1
|
idx = len(self._usedNotes) + 1
|
||||||
xR = ET.Element(_wTag("r"))
|
xR = xmlElement(_wTag("r"))
|
||||||
rPr = xmlSubElem(xR, _wTag("rPr"))
|
rPr = xmlSubElem(xR, _wTag("rPr"))
|
||||||
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
|
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
|
||||||
xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
|
xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
|
||||||
@@ -506,11 +506,11 @@ class ToDocX(Tokenizer):
|
|||||||
return xR
|
return xR
|
||||||
return None
|
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."""
|
"""Generate a data field XML object."""
|
||||||
if key and (field := key.partition(":")[2]):
|
if key and (field := key.partition(":")[2]):
|
||||||
xR = ET.Element(_wTag("r"))
|
xR = self._textRunToXml(None, fmt, "", "")
|
||||||
xT = xmlSubElem(xR, _wTag("t"), "0")
|
xT = _wText(xR, "0")
|
||||||
self._usedFields.append((xT, field))
|
self._usedFields.append((xT, field))
|
||||||
return xR
|
return xR
|
||||||
return None
|
return None
|
||||||
@@ -687,7 +687,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _appXml(self) -> str:
|
def _appXml(self) -> str:
|
||||||
"""Populate app.xml."""
|
"""Populate app.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element("Properties", attrib={
|
xRoot = xmlElement("Properties", attrib={
|
||||||
"xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties"
|
"xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties"
|
||||||
})
|
})
|
||||||
self._rels["app.xml"] = DocXXmlRel(
|
self._rels["app.xml"] = DocXXmlRel(
|
||||||
@@ -716,7 +716,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _coreXml(self) -> str:
|
def _coreXml(self) -> str:
|
||||||
"""Populate app.xml."""
|
"""Populate app.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_mkTag("cp", "coreProperties"))
|
xRoot = xmlElement(_mkTag("cp", "coreProperties"))
|
||||||
self._rels["core.xml"] = DocXXmlRel(
|
self._rels["core.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
|
relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
|
||||||
@@ -742,7 +742,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _stylesXml(self) -> str:
|
def _stylesXml(self) -> str:
|
||||||
"""Populate styles.xml."""
|
"""Populate styles.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("styles"))
|
xRoot = xmlElement(_wTag("styles"))
|
||||||
self._rels["styles.xml"] = DocXXmlRel(
|
self._rels["styles.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{RELS_BASE}/styles",
|
relType=f"{RELS_BASE}/styles",
|
||||||
@@ -830,7 +830,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _defaultHeaderXml(self) -> str:
|
def _defaultHeaderXml(self) -> str:
|
||||||
"""Populate header1.xml."""
|
"""Populate header1.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("hdr"))
|
xRoot = xmlElement(_wTag("hdr"))
|
||||||
self._rels["header1.xml"] = DocXXmlRel(
|
self._rels["header1.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{RELS_BASE}/header",
|
relType=f"{RELS_BASE}/header",
|
||||||
@@ -877,7 +877,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _firstHeaderXml(self) -> str:
|
def _firstHeaderXml(self) -> str:
|
||||||
"""Populate header2.xml."""
|
"""Populate header2.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("hdr"))
|
xRoot = xmlElement(_wTag("hdr"))
|
||||||
self._rels["header2.xml"] = DocXXmlRel(
|
self._rels["header2.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{RELS_BASE}/header",
|
relType=f"{RELS_BASE}/header",
|
||||||
@@ -902,7 +902,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str:
|
def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str:
|
||||||
"""Populate document.xml."""
|
"""Populate document.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("document"))
|
xRoot = xmlElement(_wTag("document"))
|
||||||
xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml")
|
xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml")
|
||||||
xBody = xmlSubElem(xRoot, _wTag("body"))
|
xBody = xmlSubElem(xRoot, _wTag("body"))
|
||||||
self._rels["document.xml"] = DocXXmlRel(
|
self._rels["document.xml"] = DocXXmlRel(
|
||||||
@@ -978,7 +978,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _footnotesXml(self) -> str:
|
def _footnotesXml(self) -> str:
|
||||||
"""Populate footnotes.xml."""
|
"""Populate footnotes.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("footnotes"))
|
xRoot = xmlElement(_wTag("footnotes"))
|
||||||
self._rels["footnotes.xml"] = DocXXmlRel(
|
self._rels["footnotes.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{RELS_BASE}/footnotes",
|
relType=f"{RELS_BASE}/footnotes",
|
||||||
@@ -1001,7 +1001,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _fontTableXml(self) -> str:
|
def _fontTableXml(self) -> str:
|
||||||
"""Populate fontTable.xml."""
|
"""Populate fontTable.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("fonts"))
|
xRoot = xmlElement(_wTag("fonts"))
|
||||||
self._rels["fontTable.xml"] = DocXXmlRel(
|
self._rels["fontTable.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{RELS_BASE}/fontTable",
|
relType=f"{RELS_BASE}/fontTable",
|
||||||
@@ -1024,7 +1024,7 @@ class ToDocX(Tokenizer):
|
|||||||
def _settingsXml(self) -> str:
|
def _settingsXml(self) -> str:
|
||||||
"""Populate settings.xml."""
|
"""Populate settings.xml."""
|
||||||
rId = self._nextRelId()
|
rId = self._nextRelId()
|
||||||
xRoot = ET.Element(_wTag("settings"))
|
xRoot = xmlElement(_wTag("settings"))
|
||||||
self._rels["settings.xml"] = DocXXmlRel(
|
self._rels["settings.xml"] = DocXXmlRel(
|
||||||
rId=rId,
|
rId=rId,
|
||||||
relType=f"{RELS_BASE}/settings",
|
relType=f"{RELS_BASE}/settings",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
|
|||||||
from PyQt5.QtGui import QColor, QFont
|
from PyQt5.QtGui import QColor, QFont
|
||||||
|
|
||||||
from novelwriter import __version__
|
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.constants import nwHeadFmt, nwStyles
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape
|
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",
|
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
|
||||||
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
|
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
|
||||||
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta: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",
|
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
||||||
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
||||||
"text": "urn:oasis:names:tc:opendocument:xmlns:text: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_TEXT = "Text_20_body"
|
||||||
S_META = "Text_20_Meta"
|
S_META = "Text_20_Meta"
|
||||||
S_HNF = "Header_20_and_20_Footer"
|
S_HNF = "Header_20_and_20_Footer"
|
||||||
|
S_NUM = "N0"
|
||||||
|
|
||||||
# Font Data
|
# Font Data
|
||||||
FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
|
FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
|
||||||
@@ -513,13 +515,14 @@ class ToOdt(Tokenizer):
|
|||||||
oVers = _mkTag("office", "version")
|
oVers = _mkTag("office", "version")
|
||||||
xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
|
xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
|
||||||
|
|
||||||
def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
|
def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
|
||||||
with zipObj.open(name, mode="w") as fObj:
|
zipObj.writestr(
|
||||||
xml = ET.ElementTree(xObj)
|
name, ET.tostring(root, encoding="utf-8", xml_declaration=True),
|
||||||
xml.write(fObj, encoding="utf-8", xml_declaration=True)
|
compress_type=ZIP_DEFLATED, compresslevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
|
with ZipFile(path, mode="w") as outZip:
|
||||||
outZip.writestr("mimetype", X_MIME)
|
outZip.writestr("mimetype", X_MIME, compress_type=None, compresslevel=None)
|
||||||
xmlToZip("META-INF/manifest.xml", xMani, outZip)
|
xmlToZip("META-INF/manifest.xml", xMani, outZip)
|
||||||
xmlToZip("settings.xml", xSett, outZip)
|
xmlToZip("settings.xml", xSett, outZip)
|
||||||
xmlToZip("content.xml", self._dCont, outZip)
|
xmlToZip("content.xml", self._dCont, outZip)
|
||||||
@@ -627,7 +630,7 @@ class ToOdt(Tokenizer):
|
|||||||
elif fFmt == TextFmt.FNOTE:
|
elif fFmt == TextFmt.FNOTE:
|
||||||
xNode = self._generateFootnote(fData)
|
xNode = self._generateFootnote(fData)
|
||||||
elif fFmt == TextFmt.FIELD:
|
elif fFmt == TextFmt.FIELD:
|
||||||
xNode = self._generateField(fData)
|
xNode = self._generateField(fData, xFmt)
|
||||||
elif fFmt == TextFmt.STRIP:
|
elif fFmt == TextFmt.STRIP:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -713,24 +716,31 @@ class ToOdt(Tokenizer):
|
|||||||
if content := self._footnotes.get(key):
|
if content := self._footnotes.get(key):
|
||||||
self._nNote += 1
|
self._nNote += 1
|
||||||
nStyle = ODTParagraphStyle("New")
|
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", "id"): f"ftn{self._nNote}",
|
||||||
_mkTag("text", "note-class"): "footnote",
|
_mkTag("text", "note-class"): "footnote",
|
||||||
})
|
})
|
||||||
xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote)
|
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])
|
self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
|
||||||
return xNote
|
return xNote
|
||||||
return None
|
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."""
|
"""Generate a data field XML object."""
|
||||||
if key and (field := key.partition(":")[2]):
|
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:]}",
|
_mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}",
|
||||||
})
|
})
|
||||||
xField.text = "0"
|
if fmt == 0x00:
|
||||||
return xField
|
return xField
|
||||||
|
else:
|
||||||
|
xSpan = xmlElement(TAG_SPAN, "", tail="", attrib={
|
||||||
|
_mkTag("text", "style-name"): self._textStyle(fmt),
|
||||||
|
})
|
||||||
|
xSpan.append(xField)
|
||||||
|
return xSpan
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _emToCm(self, value: float) -> str:
|
def _emToCm(self, value: float) -> str:
|
||||||
@@ -836,6 +846,14 @@ class ToOdt(Tokenizer):
|
|||||||
_mkTag("style", "class"): "extra",
|
_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
|
return
|
||||||
|
|
||||||
def _useableStyles(self) -> None:
|
def _useableStyles(self) -> None:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<w:t xml:space="preserve">Word Count: </w:t>
|
<w:t xml:space="preserve">Word Count: </w:t>
|
||||||
</w:r>
|
</w:r>
|
||||||
<w:r>
|
<w:r>
|
||||||
|
<w:rPr />
|
||||||
<w:t>4,035</w:t>
|
<w:t>4,035</w:t>
|
||||||
</w:r>
|
</w:r>
|
||||||
</w:p>
|
</w:p>
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
<w:t xml:space="preserve">Character Count: </w:t>
|
<w:t xml:space="preserve">Character Count: </w:t>
|
||||||
</w:r>
|
</w:r>
|
||||||
<w:r>
|
<w:r>
|
||||||
|
<w:rPr />
|
||||||
<w:t>27,064</w:t>
|
<w:t>27,064</w:t>
|
||||||
</w:r>
|
</w:r>
|
||||||
</w:p>
|
</w:p>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||||
<office:meta>
|
<office:meta>
|
||||||
<meta:creation-date>2024-10-28T20:17:34</meta:creation-date>
|
<meta:creation-date>2024-10-29T09:33:22</meta:creation-date>
|
||||||
<meta:generator>novelWriter/2.6a3</meta:generator>
|
<meta:generator>novelWriter/2.6a3</meta:generator>
|
||||||
<meta:initial-creator>Jane Smith</meta:initial-creator>
|
<meta:initial-creator>Jane Smith</meta:initial-creator>
|
||||||
<meta:editing-cycles>1234</meta:editing-cycles>
|
<meta:editing-cycles>1234</meta:editing-cycles>
|
||||||
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
|
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
|
||||||
<dc:title>Test Project</dc:title>
|
<dc:title>Test Project</dc:title>
|
||||||
<dc:date>2024-10-28T20:17:34</dc:date>
|
<dc:date>2024-10-29T09:33:22</dc:date>
|
||||||
<dc:creator>Jane Smith</dc:creator>
|
<dc:creator>Jane Smith</dc:creator>
|
||||||
</office:meta>
|
</office:meta>
|
||||||
<office:font-face-decls>
|
<office:font-face-decls>
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
|
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
|
||||||
</style:style>
|
</style:style>
|
||||||
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
||||||
|
<number:number-style style:name="N0">
|
||||||
|
<number:number number:min-integer-digits="1" />
|
||||||
|
</number:number-style>
|
||||||
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
|
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
|
||||||
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.246cm" fo:line-height="115%" fo:text-align="left" />
|
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.246cm" fo:line-height="115%" fo:text-align="left" />
|
||||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<office:document-styles xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3">
|
<office:document-styles xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3">
|
||||||
<office:font-face-decls>
|
<office:font-face-decls>
|
||||||
<style:font-face style:name="Liberation Serif" style:font-pitch="variable" />
|
<style:font-face style:name="Liberation Serif" style:font-pitch="variable" />
|
||||||
</office:font-face-decls>
|
</office:font-face-decls>
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
|
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
|
||||||
</style:style>
|
</style:style>
|
||||||
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
||||||
|
<number:number-style style:name="N0">
|
||||||
|
<number:number number:min-integer-digits="1" />
|
||||||
|
</number:number-style>
|
||||||
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
|
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
|
||||||
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.246cm" fo:line-height="115%" fo:text-align="left" />
|
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.246cm" fo:line-height="115%" fo:text-align="left" />
|
||||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||||
<office:meta>
|
<office:meta>
|
||||||
<meta:creation-date>2024-10-28T18:27:00</meta:creation-date>
|
<meta:creation-date>2024-10-29T09:41:30</meta:creation-date>
|
||||||
<meta:generator>novelWriter/2.6a3</meta:generator>
|
<meta:generator>novelWriter/2.6a3</meta:generator>
|
||||||
<meta:initial-creator>lipsum.com</meta:initial-creator>
|
<meta:initial-creator>lipsum.com</meta:initial-creator>
|
||||||
<meta:editing-cycles>50</meta:editing-cycles>
|
<meta:editing-cycles>50</meta:editing-cycles>
|
||||||
<meta:editing-duration>P0DT0H40M48S</meta:editing-duration>
|
<meta:editing-duration>P0DT0H40M48S</meta:editing-duration>
|
||||||
<dc:title>Lorem Ipsum</dc:title>
|
<dc:title>Lorem Ipsum</dc:title>
|
||||||
<dc:date>2024-10-28T18:27:00</dc:date>
|
<dc:date>2024-10-29T09:41:30</dc:date>
|
||||||
<dc:creator>lipsum.com</dc:creator>
|
<dc:creator>lipsum.com</dc:creator>
|
||||||
</office:meta>
|
</office:meta>
|
||||||
<office:font-face-decls>
|
<office:font-face-decls>
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
|
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
|
||||||
</style:style>
|
</style:style>
|
||||||
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
||||||
|
<number:number-style style:name="N0">
|
||||||
|
<number:number number:min-integer-digits="1" />
|
||||||
|
</number:number-style>
|
||||||
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
|
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
|
||||||
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.320cm" fo:line-height="150%" fo:text-align="left" />
|
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.320cm" fo:line-height="150%" fo:text-align="left" />
|
||||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" />
|
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" />
|
||||||
@@ -164,8 +167,8 @@
|
|||||||
</text:user-field-decls>
|
</text:user-field-decls>
|
||||||
<text:p text:style-name="Title">Lorem Ipsum</text:p>
|
<text:p text:style-name="Title">Lorem Ipsum</text:p>
|
||||||
<text:p text:style-name="P1"><text:span text:style-name="T1">By lipsum.com</text:span></text:p>
|
<text:p text:style-name="P1"><text:span text:style-name="T1">By lipsum.com</text:span></text:p>
|
||||||
<text:p text:style-name="P1">Word Count: <text:user-field-get text:name="ManuscriptAllWords">0</text:user-field-get></text:p>
|
<text:p text:style-name="P1">Word Count: <text:user-field-get style:data-style-name="N0" text:name="ManuscriptAllWords">0</text:user-field-get></text:p>
|
||||||
<text:p text:style-name="P1">Character Count: <text:user-field-get text:name="ManuscriptAllChars">0</text:user-field-get></text:p>
|
<text:p text:style-name="P1">Character Count: <text:user-field-get style:data-style-name="N0" text:name="ManuscriptAllChars">0</text:user-field-get></text:p>
|
||||||
<text:p text:style-name="P1">“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”</text:p>
|
<text:p text:style-name="P1">“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”</text:p>
|
||||||
<text:p text:style-name="P1">“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”</text:p>
|
<text:p text:style-name="P1">“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”</text:p>
|
||||||
<text:p text:style-name="P2"><text:span text:style-name="T2">Comment:</text:span> <text:span text:style-name="T3">Exctracted from the lipsum.com website.</text:span></text:p>
|
<text:p text:style-name="P2"><text:span text:style-name="T2">Comment:</text:span> <text:span text:style-name="T3">Exctracted from the lipsum.com website.</text:span></text:p>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ from novelwriter.common import (
|
|||||||
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle,
|
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle,
|
||||||
isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag,
|
isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag,
|
||||||
jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath,
|
jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath,
|
||||||
readTextFile, simplified, transferCase, uniqueCompact, xmlIndent,
|
readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
|
||||||
xmlSubElem, yesNo
|
xmlIndent, xmlSubElem, yesNo
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.mocked import causeOSError
|
from tests.mocked import causeOSError
|
||||||
@@ -634,6 +634,29 @@ def testBaseCommon_xmlIndent():
|
|||||||
assert data == "foobar"
|
assert data == "foobar"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_xmlElement():
|
||||||
|
"""Test the xmlElement function."""
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlElement("node", None, attrib={"a": "b"})
|
||||||
|
) == b'<node a="b" />'
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlElement("node", "text", attrib={"a": "b"})
|
||||||
|
) == b'<node a="b">text</node>'
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlElement("node", "text", tail="foo", attrib={"a": "b"})
|
||||||
|
) == b'<node a="b">text</node>foo'
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlElement("node", 42, attrib={"a": "b"})
|
||||||
|
) == b'<node a="b">42</node>'
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlElement("node", 3.14, attrib={"a": "b"})
|
||||||
|
) == b'<node a="b">3.14</node>'
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlElement("node", True, attrib={"a": "b"})
|
||||||
|
) == b'<node a="b">true</node>'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_xmlSubElem():
|
def testBaseCommon_xmlSubElem():
|
||||||
"""Test the xmlSubElem function."""
|
"""Test the xmlSubElem function."""
|
||||||
@@ -643,6 +666,9 @@ def testBaseCommon_xmlSubElem():
|
|||||||
assert ET.tostring(
|
assert ET.tostring(
|
||||||
xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})
|
xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})
|
||||||
) == b'<node a="b">text</node>'
|
) == b'<node a="b">text</node>'
|
||||||
|
assert ET.tostring(
|
||||||
|
xmlSubElem(ET.Element("r"), "node", "text", tail="foo", attrib={"a": "b"})
|
||||||
|
) == b'<node a="b">text</node>foo'
|
||||||
assert ET.tostring(
|
assert ET.tostring(
|
||||||
xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})
|
xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})
|
||||||
) == b'<node a="b">42</node>'
|
) == b'<node a="b">42</node>'
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath):
|
def testCoreDocBuild_DocX(mockGUI, prjLipsum, fncPath):
|
||||||
"""Test building a Word manuscript."""
|
"""Test building a Word manuscript."""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
project.openProject(prjLipsum)
|
project.openProject(prjLipsum)
|
||||||
@@ -352,22 +352,41 @@ def testCoreDocBuild_DocX(monkeypatch, mockGUI, prjLipsum, fncPath):
|
|||||||
assert docFile.is_file()
|
assert docFile.is_file()
|
||||||
assert zipfile.is_zipfile(docFile)
|
assert zipfile.is_zipfile(docFile)
|
||||||
|
|
||||||
# Check Error Handling
|
|
||||||
# ====================
|
|
||||||
|
|
||||||
with monkeypatch.context() as mp:
|
@pytest.mark.core
|
||||||
mp.setattr("builtins.open", causeOSError)
|
def testCoreDocBuild_PDF(mockGUI, prjLipsum, fncPath):
|
||||||
|
"""Test building a PDF manuscript."""
|
||||||
|
project = NWProject()
|
||||||
|
project.openProject(prjLipsum)
|
||||||
|
|
||||||
docFile = fncPath / "Lorem Ipsum Err.fodt"
|
build = BuildSettings()
|
||||||
for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.FODT):
|
build.unpack(BUILD_CONF)
|
||||||
pass
|
|
||||||
|
|
||||||
assert docBuild.error == "OSError: Mock OSError"
|
docBuild = NWBuildDocument(project, build)
|
||||||
assert not docFile.is_file()
|
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
|
@pytest.mark.core
|
||||||
def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
|
def testCoreDocBuild_NWD(mockGUI, prjLipsum, fncPath, tstPaths):
|
||||||
"""Test building a NWD manuscript."""
|
"""Test building a NWD manuscript."""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
project.openProject(prjLipsum)
|
project.openProject(prjLipsum)
|
||||||
@@ -420,19 +439,6 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
|
|||||||
copyfile(docFile, tstFile)
|
copyfile(docFile, tstFile)
|
||||||
assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
|
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
|
@pytest.mark.core
|
||||||
def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
|
def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
|
||||||
|
|||||||
Reference in New Issue
Block a user