diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index b97d62ad..7fce8e92 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -178,10 +178,7 @@ class NWBuildDocument:
self._cache = makeObj
try:
- if isFlat:
- makeObj.saveFlatXML(path)
- else:
- makeObj.saveOpenDocText(path)
+ makeObj.saveDocument(path)
except Exception as exc:
logException()
self._error = formatException(exc)
@@ -213,10 +210,7 @@ class NWBuildDocument:
if isinstance(path, Path):
try:
- if asJson:
- makeObj.saveHtmlJson(path)
- else:
- makeObj.saveHtml5(path)
+ makeObj.saveDocument(path, asJson=asJson)
except Exception as exc:
logException()
self._error = formatException(exc)
@@ -246,7 +240,7 @@ class NWBuildDocument:
self._cache = makeObj
try:
- makeObj.saveMarkdown(path)
+ makeObj.saveDocument(path)
except Exception as exc:
logException()
self._error = formatException(exc)
@@ -276,10 +270,7 @@ class NWBuildDocument:
if isinstance(path, Path):
try:
- if asJson:
- makeObj.saveRawMarkdownJSON(path)
- else:
- makeObj.saveRawMarkdown(path)
+ makeObj.saveRawDocument(path, asJson=asJson)
except Exception as exc:
logException()
self._error = formatException(exc)
diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py
index cdf30700..78f359b9 100644
--- a/novelwriter/formats/tohtml.py
+++ b/novelwriter/formats/tohtml.py
@@ -290,51 +290,51 @@ class ToHtml(Tokenizer):
return
- def saveHtml5(self, path: str | Path) -> None:
+ def saveDocument(self, path: str | Path, asJson: bool = False) -> None:
"""Save the data to an HTML file."""
- with open(path, mode="w", encoding="utf-8") as fObj:
- fObj.write((
- "\n"
- "\n"
- "
\n"
- "\n"
- "{title:s}\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "{body:s}\n"
- "\n"
- "\n"
- "\n"
- ).format(
- title=self._project.data.name,
- style="\n".join(self.getStyleSheet()),
- body=("".join(self._fullHTML)).replace("\t", " ").rstrip(),
- ))
- logger.info("Wrote file: %s", path)
- return
-
- def saveHtmlJson(self, path: str | Path) -> None:
- """Save the data to a JSON file."""
- timeStamp = time()
- data = {
- "meta": {
- "projectName": self._project.data.name,
- "novelAuthor": self._project.data.author,
- "buildTime": int(timeStamp),
- "buildTimeStr": formatTimeStamp(timeStamp),
- },
- "text": {
- "css": self.getStyleSheet(),
- "html": [t.replace("\t", " ").rstrip().split("\n") for t in self.fullHTML],
+ if asJson:
+ ts = time()
+ data = {
+ "meta": {
+ "projectName": self._project.data.name,
+ "novelAuthor": self._project.data.author,
+ "buildTime": int(ts),
+ "buildTimeStr": formatTimeStamp(ts),
+ },
+ "text": {
+ "css": self.getStyleSheet(),
+ "html": [t.replace("\t", " ").rstrip().split("\n") for t in self.fullHTML],
+ }
}
- }
- with open(path, mode="w", encoding="utf-8") as fObj:
- json.dump(data, fObj, indent=2)
+ with open(path, mode="w", encoding="utf-8") as fObj:
+ json.dump(data, fObj, indent=2)
+
+ else:
+ with open(path, mode="w", encoding="utf-8") as fObj:
+ fObj.write((
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "{title:s}\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "{body:s}\n"
+ "\n"
+ "\n"
+ "\n"
+ ).format(
+ title=self._project.data.name,
+ style="\n".join(self.getStyleSheet()),
+ body=("".join(self._fullHTML)).replace("\t", " ").rstrip(),
+ ))
+
logger.info("Wrote file: %s", path)
+
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py
index a20d8114..ea003b4c 100644
--- a/novelwriter/formats/tokenizer.py
+++ b/novelwriter/formats/tokenizer.py
@@ -486,6 +486,10 @@ class Tokenizer(ABC):
def doConvert(self) -> None:
raise NotImplementedError
+ @abstractmethod
+ def saveDocument(self, path: str | Path) -> None:
+ raise NotImplementedError
+
def addRootHeading(self, tHandle: str) -> None:
"""Add a heading at the start of a new root folder."""
self._text = ""
@@ -1087,29 +1091,31 @@ class Tokenizer(ABC):
return
- def saveRawMarkdown(self, path: str | Path) -> None:
+ def saveRawDocument(self, path: str | Path, asJson: bool = False) -> None:
"""Save the raw text to a plain text file."""
- with open(path, mode="w", encoding="utf-8") as outFile:
- for nwdPage in self._markdown:
- outFile.write(nwdPage)
- return
-
- def saveRawMarkdownJSON(self, path: str | Path) -> None:
- """Save the raw text to a JSON file."""
- timeStamp = time()
- data = {
- "meta": {
- "projectName": self._project.data.name,
- "novelAuthor": self._project.data.author,
- "buildTime": int(timeStamp),
- "buildTimeStr": formatTimeStamp(timeStamp),
- },
- "text": {
- "nwd": [page.rstrip("\n").split("\n") for page in self._markdown],
+ if asJson:
+ ts = time()
+ data = {
+ "meta": {
+ "projectName": self._project.data.name,
+ "novelAuthor": self._project.data.author,
+ "buildTime": int(ts),
+ "buildTimeStr": formatTimeStamp(ts),
+ },
+ "text": {
+ "nwd": [page.rstrip("\n").split("\n") for page in self._markdown],
+ }
}
- }
- with open(path, mode="w", encoding="utf-8") as fObj:
- json.dump(data, fObj, indent=2)
+ with open(path, mode="w", encoding="utf-8") as fObj:
+ json.dump(data, fObj, indent=2)
+
+ else:
+ with open(path, mode="w", encoding="utf-8") as outFile:
+ for nwdPage in self._markdown:
+ outFile.write(nwdPage)
+
+ logger.info("Wrote file: %s", path)
+
return
##
diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py
index 7a9d768f..a738513c 100644
--- a/novelwriter/formats/tomarkdown.py
+++ b/novelwriter/formats/tomarkdown.py
@@ -199,7 +199,7 @@ class ToMarkdown(Tokenizer):
return
- def saveMarkdown(self, path: str | Path) -> None:
+ def saveDocument(self, path: str | Path) -> None:
"""Save the data to a plain text file."""
with open(path, mode="w", encoding="utf-8") as outFile:
outFile.write("".join(self._fullMD))
diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py
index 51f8b6af..8d3de780 100644
--- a/novelwriter/formats/toodt.py
+++ b/novelwriter/formats/toodt.py
@@ -543,46 +543,44 @@ class ToOdt(Tokenizer):
self._xText.insert(0, xFields)
return
- def saveFlatXML(self, path: str | Path) -> None:
- """Save the data to an .fodt file."""
- with open(path, mode="wb") as fObj:
- xml = ET.ElementTree(self._dFlat)
- xmlIndent(xml)
- xml.write(fObj, encoding="utf-8", xml_declaration=True)
- logger.info("Wrote file: %s", path)
- return
-
- def saveOpenDocText(self, path: str | Path) -> None:
- """Save the data to an .odt file."""
- mMani = _mkTag("manifest", "manifest")
- mVers = _mkTag("manifest", "version")
- mPath = _mkTag("manifest", "full-path")
- mType = _mkTag("manifest", "media-type")
- mFile = _mkTag("manifest", "file-entry")
-
- xMani = ET.Element(mMani, attrib={mVers: X_VERS})
- ET.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
- ET.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
- ET.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
- ET.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
- ET.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
-
- oRoot = _mkTag("office", "document-settings")
- oVers = _mkTag("office", "version")
- xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
-
- def putInZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
- with zipObj.open(name, mode="w") as fObj:
- xml = ET.ElementTree(xObj)
+ def saveDocument(self, path: str | Path) -> None:
+ """Save the data to an .fodt or .odt file."""
+ if self._isFlat:
+ with open(path, mode="wb") as fObj:
+ xml = ET.ElementTree(self._dFlat)
+ xmlIndent(xml)
xml.write(fObj, encoding="utf-8", xml_declaration=True)
- with ZipFile(path, mode="w") as outZip:
- outZip.writestr("mimetype", X_MIME)
- putInZip("META-INF/manifest.xml", xMani, outZip)
- putInZip("settings.xml", xSett, outZip)
- putInZip("content.xml", self._dCont, outZip)
- putInZip("meta.xml", self._dMeta, outZip)
- putInZip("styles.xml", self._dStyl, outZip)
+ else:
+ mMani = _mkTag("manifest", "manifest")
+ mVers = _mkTag("manifest", "version")
+ mPath = _mkTag("manifest", "full-path")
+ mType = _mkTag("manifest", "media-type")
+ mFile = _mkTag("manifest", "file-entry")
+
+ xMani = ET.Element(mMani, attrib={mVers: X_VERS})
+ ET.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
+ ET.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
+ ET.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
+ ET.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
+ ET.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
+
+ oRoot = _mkTag("office", "document-settings")
+ oVers = _mkTag("office", "version")
+ xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
+
+ def putInZip(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)
+
+ with ZipFile(path, mode="w") as outZip:
+ outZip.writestr("mimetype", X_MIME)
+ putInZip("META-INF/manifest.xml", xMani, outZip)
+ putInZip("settings.xml", xSett, outZip)
+ putInZip("content.xml", self._dCont, outZip)
+ putInZip("meta.xml", self._dMeta, outZip)
+ putInZip("styles.xml", self._dStyl, outZip)
logger.info("Wrote file: %s", path)
diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py
index 593a8fe4..43f10b69 100644
--- a/novelwriter/formats/toqdoc.py
+++ b/novelwriter/formats/toqdoc.py
@@ -25,6 +25,8 @@ from __future__ import annotations
import logging
+from pathlib import Path
+
from PyQt5.QtGui import (
QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument
@@ -273,6 +275,10 @@ class ToQTextDocument(Tokenizer):
return
+ def saveDocument(self, path: str | Path) -> None:
+ """Not implemented."""
+ return
+
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
diff --git a/tests/test_formats/test_fmt_tohtml.py b/tests/test_formats/test_fmt_tohtml.py
index 56ceff47..49e7bf63 100644
--- a/tests/test_formats/test_fmt_tohtml.py
+++ b/tests/test_formats/test_fmt_tohtml.py
@@ -644,12 +644,12 @@ def testFmtToHtml_Save(mockGUI, fncPath):
)
saveFile = fncPath / "outFile.htm"
- html.saveHtml5(saveFile)
+ html.saveDocument(saveFile, asJson=False)
assert saveFile.read_text(encoding="utf-8") == htmlDoc
# JSON + HTML
saveFile = fncPath / "outFile.json"
- html.saveHtmlJson(saveFile)
+ html.saveDocument(saveFile, asJson=True)
data = json.loads(saveFile.read_text(encoding="utf-8"))
assert data["meta"]["projectName"] == ""
assert data["meta"]["novelAuthor"] == ""
diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py
index 819e51c0..33531f02 100644
--- a/tests/test_formats/test_fmt_tokenizer.py
+++ b/tests/test_formats/test_fmt_tokenizer.py
@@ -39,6 +39,9 @@ class BareTokenizer(Tokenizer):
def doConvert(self):
super().doConvert() # type: ignore (deliberate check)
+ def saveDocument(self, path) -> None:
+ super().saveDocument(path) # type: ignore (deliberate check)
+
@pytest.mark.core
def testFmtToken_Setters(mockGUI):
@@ -219,12 +222,12 @@ def testFmtToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
# Save File
savePath = fncPath / "dump.nwd"
- tokens.saveRawMarkdown(savePath)
+ tokens.saveRawDocument(savePath, asJson=False)
assert readFile(savePath) == (
"#! Notes: Plot\n\n"
"#! Notes: Plot\n\n"
)
- tokens.saveRawMarkdownJSON(savePath)
+ tokens.saveRawDocument(savePath, asJson=True)
assert json.loads(readFile(savePath))["text"] == {
"nwd": [
["#! Notes: Plot"],
@@ -232,10 +235,13 @@ def testFmtToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
]
}
- # Check abstract method
+ # Check abstract methods
with pytest.raises(NotImplementedError):
tokens.doConvert()
+ with pytest.raises(NotImplementedError):
+ tokens.saveDocument(fncPath)
+
@pytest.mark.core
def testFmtToken_StripEscape():
diff --git a/tests/test_formats/test_fmt_tomarkdown.py b/tests/test_formats/test_fmt_tomarkdown.py
index d5ac9256..5f446699 100644
--- a/tests/test_formats/test_fmt_tomarkdown.py
+++ b/tests/test_formats/test_fmt_tomarkdown.py
@@ -293,7 +293,7 @@ def testFmtToMarkdown_Save(mockGUI, fncPath):
# ==========
saveFile = fncPath / "outFile.md"
- toMD.saveMarkdown(saveFile)
+ toMD.saveDocument(saveFile)
assert saveFile.read_text(encoding="utf-8") == "".join(resText)
diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py
index 3e02d538..3206b221 100644
--- a/tests/test_formats/test_fmt_toodt.py
+++ b/tests/test_formats/test_fmt_toodt.py
@@ -802,7 +802,7 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
testFile = tstPaths.outDir / "coreToOdt_SaveFlat_document.fodt"
compFile = tstPaths.refDir / "coreToOdt_SaveFlat_document.fodt"
- odt.saveFlatXML(flatFile)
+ odt.saveDocument(flatFile)
assert flatFile.exists()
copyfile(flatFile, testFile)
@@ -837,7 +837,7 @@ def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths):
fullFile = fncPath / "document.odt"
- odt.saveOpenDocText(fullFile)
+ odt.saveDocument(fullFile)
assert fullFile.exists()
assert zipfile.is_zipfile(fullFile)
diff --git a/tests/test_formats/test_fmt_toqdoc.py b/tests/test_formats/test_fmt_toqdoc.py
index 62211ae1..d6a65e7b 100644
--- a/tests/test_formats/test_fmt_toqdoc.py
+++ b/tests/test_formats/test_fmt_toqdoc.py
@@ -61,6 +61,7 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
+ qdoc.saveDocument("") # Doesn't do anything for this format
qdoc._isNovel = True
qdoc._isFirst = True