""" novelWriter – ToDocX Class Tester ================================= This file is a part of novelWriter Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # noqa from __future__ import annotations import xml.etree.ElementTree as ET import zipfile import pytest from novelwriter.common import xmlIndent from novelwriter.constants import nwHeadFmt from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt, nwComment from novelwriter.formats.shared import BlockFmt, BlockTyp from novelwriter.formats.todocx import OOXML_SCM, ToDocX, _mkTag, _wTag from tests.tools import DOCX_IGNORE, cmpFiles, xmlToText @pytest.mark.core def testFmtToDocX_NovelHeadingStyles(mockGUI): """Test formatting of novel headings.""" project = NWProject() doc = ToDocX(project) doc._isNovel = True doc.initDocument() # Title # ===== xTest = ET.Element(_wTag("body")) doc._text = "#! Hello World" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Partition # ========= doc._text = "# Hello World" # Plain xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Hello World' ) # Formatted xTest = ET.Element(_wTag("body")) doc.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}") doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'PartHello World' ) # Chapter # ======= doc._text = "## Hello World" # Plain xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Hello World' ) # Formatted xTest = ET.Element(_wTag("body")) doc.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}") doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Chapter 2Hello World' ) # Scene # ===== doc._text = "### Hello World" # Plain xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Formatted xTest = ET.Element(_wTag("body")) doc.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}") doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Scene 2Hello World' ) # Section # ======= doc._text = "#### Hello World" xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) @pytest.mark.core def testFmtToDocX_NotesHeadingStyles(mockGUI): """Test formatting of notes headings.""" project = NWProject() doc = ToDocX(project) doc._isNovel = False doc.initDocument() # Title # ===== xTest = ET.Element(_wTag("body")) doc._text = "#! Hello World" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Heading 1 # ========= doc._text = "# Hello World" xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Heading 2 # ========= doc._text = "## Hello World" xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Heading 3 # ========= doc._text = "### Hello World" xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Heading 4 # ========= doc._text = "#### Hello World" xTest = ET.Element(_wTag("body")) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) @pytest.mark.core def testFmtToDocX_ParagraphStyles(mockGUI): """Test formatting of paragraphs.""" project = NWProject() doc = ToDocX(project) doc.setCommentType(nwComment.PLAIN, True) doc.setCommentType(nwComment.SYNOPSIS, True) doc.setCommentType(nwComment.SHORT, True) doc.setKeywords(True) doc.initDocument() # Normal Text xTest = ET.Element(_wTag("body")) doc._text = "Hello World" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Separator xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.SEP, "", "* * *", [], BlockFmt.NONE)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '* * *' ) # Empty Paragraph xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.SKIP, "", "* * *", [], BlockFmt.NONE)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' ) # Synopsis xTest = ET.Element(_wTag("body")) doc._text = "%Synopsis: Hello World\n\n" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Synopsis:' ' ' 'Hello World' '' ) # Short xTest = ET.Element(_wTag("body")) doc._text = "%Short: Hello World\n\n" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Short Description:' ' ' 'Hello World' '' ) # Comment xTest = ET.Element(_wTag("body")) doc._text = "% Hello World\n\n" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Comment:' ' ' 'Hello World' '' ) # Tags and References (Single) xTest = ET.Element(_wTag("body")) doc._text = "@tag: Stuff" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Tag:' ' ' 'Stuff' '' ) # Tags and References (Multiple) xTest = ET.Element(_wTag("body")) doc._text = "@char: Jane, John" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Characters:' ' ' 'Jane' ', ' 'John' '' ) # Tags and References (Invalid) xTest = ET.Element(_wTag("body")) doc._text = "@stuff: Stuff" doc._pars = [] doc.tokenizeText() doc.doConvert() assert doc._pars == [] @pytest.mark.core def testFmtToDocX_Links(mockGUI): """Test formatting of links.""" project = NWProject() doc = ToDocX(project) doc.initDocument() # Register 2 links rd1 = doc._appendExternalRel("http://example.com") rd2 = doc._appendExternalRel("https://example.com") assert rd1 == "rId1" assert rd2 == "rId2" # Link 1 xTest = ET.Element(_wTag("body")) doc._text = "Foo http://example.com bar" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Foo ' '' 'http://example.com' ' bar' ) # Link 2 xTest = ET.Element(_wTag("body")) doc._text = "Foo https://example.com bar" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Foo ' '' 'https://example.com' ' bar' ) @pytest.mark.core def testFmtToDocX_ParagraphFormatting(mockGUI): """Test formatting of paragraphs.""" project = NWProject() doc = ToDocX(project) doc.setCommentType(nwComment.PLAIN, True) doc.setCommentType(nwComment.SYNOPSIS, True) doc.setKeywords(True) doc.initDocument() # Left Align xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.LEFT)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Right Align xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.RIGHT)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Center Align xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.CENTRE)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Justify xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.JUSTIFY)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' ) # Page Break Before xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.PBB)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Hello World' '' ) # Page Break After xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.PBA)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Hello World' '' '' ) # Zero Margins xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.Z_TOP | BlockFmt.Z_BTM)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Hello World' ) # Indent xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.IND_L | BlockFmt.IND_R)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Hello World' ) # First Line Indent xTest = ET.Element(_wTag("body")) doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.IND_T)] doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' '' 'Hello World' ) @pytest.mark.core def testFmtToDocX_TextFormatting(mockGUI): """Test formatting of text.""" project = NWProject() doc = ToDocX(project) doc.initDocument() # Markdown xTest = ET.Element(_wTag("body")) doc._text = "Text **bold**, _italic_, ~~strike~~." doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Text ' 'bold' ', ' 'italic' ', ' 'strike' '.' '' ) # Nested Shortcode Text, Emphasis xTest = ET.Element(_wTag("body")) doc._text = "Some [s]nested [b]bold[/b] [u]and[/u] [i]italics[/i] text[/s] text." doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Some ' 'nested ' 'bold' ' ' 'and' ' ' 'italics' ' text' ' text.' '' ) # Shortcode Text, Super/Subscript xTest = ET.Element(_wTag("body")) doc._text = "Some super[sup]script[/sup] and sub[sub]script[/sub] text." doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Some super' 'script' ' and sub' 'script' ' text.' '' ) # Shortcode Text, Underline/Highlight xTest = ET.Element(_wTag("body")) doc._text = "Some [u]underlined and [m]highlighted[/m][/u] text." doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Some ' '' 'underlined and ' '' 'highlighted' ' text.' '' ) # Hard Break xTest = ET.Element(_wTag("body")) doc._text = "Some text.\nNext line\n" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Some text.Next line' '' ) # Tab xTest = ET.Element(_wTag("body")) doc._text = "\tItem 1\tItem 2\n" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Item 1Item 2' '' ) # Tab in Format xTest = ET.Element(_wTag("body")) doc._text = "Some **bold\ttext**" doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Some ' 'boldtext' '' ) @pytest.mark.core def testFmtToDocX_Footnotes(mockGUI): """Test formatting of footnotes.""" project = NWProject() doc = ToDocX(project) doc.initDocument() # Text xTest = ET.Element(_wTag("body")) doc._text = ( "Text with one[footnote:fa], **two**[footnote:fd], " "or three[footnote:fb] footnotes.[footnote:fe]\n\n" "%footnote.fa: Footnote text A.[footnote:fc]\n\n" "%footnote.fc: This footnote is skipped.\n\n" "%footnote.fd: Another footnote.\n\n" "%footnote.fe: Again?\n\n" ) doc.tokenizeText() doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( '' 'Text with one' '' '' ', ' 'two' '' '' ', or three' ' footnotes.' '' '' '' ) # Footnotes doc._footnotesXml() assert xmlToText(doc._files["footnotes.xml"].xml) == ( '' '' '' 'Footnote text A.' '' '' 'Another footnote.' '' '' 'Again?' '' ) @pytest.mark.core def testFmtToDocX_Fields(mockGUI): """Test formatting of footnotes.""" project = NWProject() doc = ToDocX(project) doc.initDocument() # Field Builder xNode = doc._generateField("a:b", 0x00) assert isinstance(xNode, ET.Element) assert xmlToText(xNode) == "0" assert doc._usedFields == [(xNode.find(_wTag("t")), "b")] assert doc._generateField("a", 0x00) is None # Full Processing doc._text = ( "Word Count: [field:allWords]\n" "Character Count: [field:allChars]\n" "Chicken Count: [field:allChickens]\n" ) doc.tokenizeText() doc.doConvert() doc.countStats() doc._documentXml(None, None) assert xmlToText(doc._files["document.xml"].xml) == ( '' 'Word Count: ' '6' 'Character Count: ' '46' 'Chicken Count: ' '0' '' '' '' '' '' '' ) @pytest.mark.core def testFmtToDocX_SaveDocument(mockGUI, prjLipsum, fncPath, tstPaths): """Test document output.""" project = NWProject() project.openProject(prjLipsum) pageHeader = f"Page {nwHeadFmt.DOC_PAGE} - {nwHeadFmt.DOC_PROJECT} ({nwHeadFmt.DOC_AUTHOR})" build = BuildSettings() build.setValue("filter.includeNovel", True) build.setValue("filter.includeNotes", True) build.setValue("filter.includeInactive", False) build.setValue("text.includeSynopsis", True) build.setValue("text.includeComments", True) build.setValue("text.includeKeywords", True) build.setValue("format.textFont", "Source Sans Pro,12") build.setValue("format.firstLineIndent", True) build.setValue("doc.pageHeader", pageHeader) docBuild = NWBuildDocument(project, build) docBuild.queueAll() docPath = fncPath / "document.docx" assert list(docBuild.iterBuildDocument(docPath, nwBuildFmt.DOCX)) == [ (0, True), (1, True), (2, True), (3, True), (4, True), (5, False), (6, True), (7, True), (8, True), (9, False), (10, False), (11, True), (12, True), (13, True), (14, True), (15, True), (16, True), (17, True), (18, True), (19, True), (20, True), (21, False), ] assert docPath.exists() assert zipfile.is_zipfile(docPath) with zipfile.ZipFile(docPath, mode="r") as zipObj: zipObj.extractall(fncPath / "extract") def prettifyXml(inFile, outFile): with open(outFile, mode="wb") as fStream: xml = ET.parse(inFile) xmlIndent(xml) # type: ignore xml.write(fStream, encoding="utf-8", xml_declaration=True) expected = [ fncPath / "extract" / "[Content_Types].xml", fncPath / "extract" / "_rels" / ".rels", fncPath / "extract" / "docProps" / "app.xml", fncPath / "extract" / "docProps" / "core.xml", fncPath / "extract" / "word" / "_rels" / "document.xml.rels", fncPath / "extract" / "word" / "document.xml", fncPath / "extract" / "word" / "footnotes.xml", fncPath / "extract" / "word" / "header1.xml", fncPath / "extract" / "word" / "header2.xml", fncPath / "extract" / "word" / "settings.xml", fncPath / "extract" / "word" / "styles.xml", fncPath / "extract" / "word" / "fontTable.xml", ] outDir = tstPaths.outDir / "fmtToDocX_SaveDocument" outDir.mkdir() for file in expected: assert file.is_file() name = file.name.replace("[", "").replace("]", "").lstrip(".") outFile = outDir / name refFile = tstPaths.refDir / f"fmtToDocX_SaveDocument_{name}" prettifyXml(file, outFile) assert cmpFiles(outFile, refFile, ignoreStart=DOCX_IGNORE) @pytest.mark.core def testFmtToDocX_MkTag(): """Test the tag maker function.""" assert _mkTag("r", "id") == f"{{{OOXML_SCM}/officeDocument/2006/relationships}}id" assert _mkTag("w", "t") == f"{{{OOXML_SCM}/wordprocessingml/2006/main}}t" assert _mkTag("q", "t") == "t"