diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 7fce8e92..7bd43c51 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -38,7 +38,7 @@ from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt from novelwriter.error import formatException, logException from novelwriter.formats.tohtml import ToHtml -from novelwriter.formats.tokenizer import Tokenizer +from novelwriter.formats.tokenizer import Tokenizer, ToRaw from novelwriter.formats.tomarkdown import ToMarkdown from novelwriter.formats.toodt import ToOdt from novelwriter.formats.toqdoc import TextDocumentTheme, ToQTextDocument @@ -148,22 +148,61 @@ class NWBuildDocument: def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]: """Wrapper for builders based on format.""" + asJson = False + if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT): - yield from self.iterBuildOpenDocument(path, bFormat == nwBuildFmt.FODT) + makeObj = ToOdt(self._project, isFlat=(bFormat == nwBuildFmt.FODT)) + filtered = self._setupBuild(makeObj) + makeObj.initDocument() + yield from self._iterBuild(makeObj, filtered) + makeObj.closeDocument() + elif bFormat in (nwBuildFmt.HTML, nwBuildFmt.J_HTML): - yield from self.iterBuildHTML(path, asJson=bFormat == nwBuildFmt.J_HTML) + makeObj = ToHtml(self._project) + filtered = self._setupBuild(makeObj) + yield from self._iterBuild(makeObj, filtered) + makeObj.appendFootnotes() + if not self._build.getBool("html.preserveTabs"): + makeObj.replaceTabs() + asJson = (bFormat == nwBuildFmt.J_HTML) + elif bFormat in (nwBuildFmt.STD_MD, nwBuildFmt.EXT_MD): - yield from self.iterBuildMarkdown(path, bFormat == nwBuildFmt.EXT_MD) + makeObj = ToMarkdown(self._project) + makeObj.setExtendedMarkdown(bFormat == nwBuildFmt.EXT_MD) + filtered = self._setupBuild(makeObj) + yield from self._iterBuild(makeObj, filtered) + makeObj.appendFootnotes() + if self._build.getBool("format.replaceTabs"): + makeObj.replaceTabs(nSpaces=4, spaceChar=" ") + elif bFormat in (nwBuildFmt.NWD, nwBuildFmt.J_NWD): - yield from self.iterBuildNWD(path, asJson=bFormat == nwBuildFmt.J_NWD) + makeObj = ToRaw(self._project) + makeObj.setKeepMarkdown(True) + filtered = self._setupBuild(makeObj) + yield from self._iterBuild(makeObj, filtered) + + asJson = (bFormat == nwBuildFmt.J_NWD) + + self._error = None + self._cache = makeObj + + try: + if isinstance(makeObj, ToHtml | ToRaw): + makeObj.saveDocument(path, asJson=asJson) + else: + makeObj.saveDocument(path) + except Exception as exc: + logException() + self._error = formatException(exc) + return - def iterBuildOpenDocument(self, path: Path, isFlat: bool) -> Iterable[tuple[int, bool]]: - """Build an Open Document file.""" - makeObj = ToOdt(self._project, isFlat=isFlat) - filtered = self._setupBuild(makeObj) - makeObj.initDocument() + ## + # Internal Functions + ## + def _iterBuild(self, makeObj: Tokenizer, filtered: dict) -> Iterable[tuple[int, bool]]: + """Iterate over buildable documents.""" self._count = True for i, tHandle in enumerate(self._queue): self._error = None @@ -171,116 +210,8 @@ class NWBuildDocument: yield i, self._doBuild(makeObj, tHandle) else: yield i, False - - makeObj.closeDocument() - - self._error = None - self._cache = makeObj - - try: - makeObj.saveDocument(path) - except Exception as exc: - logException() - self._error = formatException(exc) - return - def iterBuildHTML(self, path: Path | None, asJson: bool = False) -> Iterable[tuple[int, bool]]: - """Build an HTML file. If path is None, no file is saved. This - is used for generating build previews. - """ - makeObj = ToHtml(self._project) - filtered = self._setupBuild(makeObj) - - self._count = False - for i, tHandle in enumerate(self._queue): - self._error = None - if filtered.get(tHandle, (False, 0))[0]: - yield i, self._doBuild(makeObj, tHandle) - else: - yield i, False - - makeObj.appendFootnotes() - - if not self._build.getBool("html.preserveTabs"): - makeObj.replaceTabs() - - self._error = None - self._cache = makeObj - - if isinstance(path, Path): - try: - makeObj.saveDocument(path, asJson=asJson) - except Exception as exc: - logException() - self._error = formatException(exc) - - return - - def iterBuildMarkdown(self, path: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]: - """Build a Markdown file.""" - makeObj = ToMarkdown(self._project) - filtered = self._setupBuild(makeObj) - - makeObj.setExtendedMarkdown(extendedMd) - if self._build.getBool("format.replaceTabs"): - makeObj.replaceTabs(nSpaces=4, spaceChar=" ") - - self._count = False - for i, tHandle in enumerate(self._queue): - self._error = None - if filtered.get(tHandle, (False, 0))[0]: - yield i, self._doBuild(makeObj, tHandle) - else: - yield i, False - - makeObj.appendFootnotes() - - self._error = None - self._cache = makeObj - - try: - makeObj.saveDocument(path) - except Exception as exc: - logException() - self._error = formatException(exc) - - return - - def iterBuildNWD(self, path: Path | None, asJson: bool = False) -> Iterable[tuple[int, bool]]: - """Build a novelWriter Markdown file.""" - makeObj = ToMarkdown(self._project) - filtered = self._setupBuild(makeObj) - - makeObj.setKeepMarkdown(True) - - self._count = False - for i, tHandle in enumerate(self._queue): - self._error = None - if filtered.get(tHandle, (False, 0))[0]: - yield i, self._doBuild(makeObj, tHandle, convert=False) - else: - yield i, False - - if self._build.getBool("format.replaceTabs"): - makeObj.replaceTabs(nSpaces=4, spaceChar=" ") - - self._error = None - self._cache = makeObj - - if isinstance(path, Path): - try: - makeObj.saveRawDocument(path, asJson=asJson) - except Exception as exc: - logException() - self._error = formatException(exc) - - return - - ## - # Internal Functions - ## - def _setupBuild(self, bldObj: Tokenizer) -> dict: """Configure the build object.""" # Get Settings diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index ea003b4c..faeeca9d 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -1245,3 +1245,41 @@ class HeadingFormatter: hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, fText) return hFormat + + +class ToRaw(Tokenizer): + + def __init__(self, project: NWProject) -> None: + super().__init__(project) + self._keepMD = True + return + + def doConvert(self) -> None: + return + + def saveDocument(self, path: str | Path, asJson: bool = False) -> None: + """Save the raw text to a plain text file.""" + 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) + + 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/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json index 333a90e1..5f8a5bf5 100644 --- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json +++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json @@ -2,8 +2,8 @@ "meta": { "projectName": "Lorem Ipsum", "novelAuthor": "lipsum.com", - "buildTime": 1714229171, - "buildTimeStr": "2024-04-27 16:46:11" + "buildTime": 1729027593, + "buildTimeStr": "2024-10-15 23:26:33" }, "text": { "nwd": [ @@ -96,11 +96,11 @@ "", "% Exctracted from the lipsum.com website.", "", - " It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.", + "\tIt is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.", "", - " The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.", + "\tThe point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.", "", - " Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)." + "\tMany desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)." ], [ "## Chapter Two", diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 391cc2b6..5c4e35e0 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -32,6 +32,7 @@ from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt from novelwriter.formats.tohtml import ToHtml +from novelwriter.formats.tokenizer import ToRaw from novelwriter.formats.tomarkdown import ToMarkdown from novelwriter.formats.toodt import ToOdt @@ -101,7 +102,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP count = 0 error = [] - for _, success in docBuild.iterBuildOpenDocument(docFile, True): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.FODT): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -119,7 +120,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP count = 0 error = [] - for _, success in docBuild.iterBuildOpenDocument(docFile, False): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.ODT): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -136,7 +137,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP mp.setattr("builtins.open", causeOSError) docFile = fncPath / "Lorem Ipsum Err.fodt" - for _ in docBuild.iterBuildOpenDocument(docFile, True): + for _ in docBuild.iterBuild(docFile, nwBuildFmt.FODT): pass assert docBuild.error == "OSError: Mock OSError" @@ -152,7 +153,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP count = 0 error = [] docFile = fncPath / "Lorem Ipsum Err.fodt" - for _, success in docBuild.iterBuildOpenDocument(docFile, True): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.FODT): count += 1 if success else 0 if not success and docBuild.error: error.append(docBuild.error) @@ -203,7 +204,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): count = 0 error = [] - for _, success in docBuild.iterBuildHTML(docFile): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.HTML): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -223,7 +224,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): count = 0 error = [] - for _, success in docBuild.iterBuildHTML(docFile, asJson=True): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.J_HTML): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -241,7 +242,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): mp.setattr("builtins.open", causeOSError) docFile = fncPath / "Lorem Ipsum Err.htm" - for _ in docBuild.iterBuildHTML(docFile): + for _ in docBuild.iterBuild(docFile, nwBuildFmt.HTML): pass assert docBuild.error == "OSError: Mock OSError" @@ -271,7 +272,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths count = 0 error = [] - for _, success in docBuild.iterBuildMarkdown(docFile, False): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.STD_MD): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -291,7 +292,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths count = 0 error = [] - for _, success in docBuild.iterBuildMarkdown(docFile, True): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.EXT_MD): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -309,7 +310,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths mp.setattr("builtins.open", causeOSError) docFile = fncPath / "Lorem Ipsum Err.md" - for _ in docBuild.iterBuildMarkdown(docFile, False): + for _ in docBuild.iterBuild(docFile, nwBuildFmt.STD_MD): pass assert docBuild.error == "OSError: Mock OSError" @@ -339,7 +340,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): count = 0 error = [] - for _, success in docBuild.iterBuildNWD(docFile, asJson=False): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.NWD): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -359,7 +360,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): count = 0 error = [] - for _, success in docBuild.iterBuildNWD(docFile, asJson=True): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.J_NWD): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -377,7 +378,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): mp.setattr("builtins.open", causeOSError) docFile = fncPath / "Lorem Ipsum Err.md" - for _ in docBuild.iterBuildNWD(docFile): + for _ in docBuild.iterBuild(docFile, nwBuildFmt.NWD): pass assert docBuild.error == "OSError: Mock OSError" @@ -401,7 +402,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path): count = 0 error = [] docFile = fncPath / "Minimal.txt" - for _, success in docBuild.iterBuildNWD(docFile, asJson=False): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.NWD): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -431,7 +432,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path): count = 0 error = [] docFile = fncPath / "Minimal.txt" - for _, success in docBuild.iterBuildNWD(docFile, asJson=False): + for _, success in docBuild.iterBuild(docFile, nwBuildFmt.NWD): count += 1 if success else 0 if docBuild.error: error.append(docBuild.error) @@ -561,7 +562,7 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd): (0, True), (1, True), (2, False), (3, True), (4, True), (5, True), (6, True), (7, True), (8, True), (9, False), ] - assert isinstance(docBuild.lastBuild, ToMarkdown) + assert isinstance(docBuild.lastBuild, ToRaw) assert docFile.read_text(encoding="utf-8") == ( "#! New Novel\n\n" "By Jane Doe\n\n" @@ -582,7 +583,7 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd): (0, True), (1, True), (2, False), (3, True), (4, True), (5, True), (6, True), (7, True), (8, True), (9, False), ] - assert isinstance(docBuild.lastBuild, ToMarkdown) + assert isinstance(docBuild.lastBuild, ToRaw) data = json.loads(docFile.read_text(encoding="utf-8")) assert "meta" in data assert "text" in data