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