diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 82ed2e36..fe634b5e 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -903,7 +903,6 @@ class ToDocX(Tokenizer): """Populate document.xml.""" rId = self._nextRelId() xRoot = xmlElement(_wTag("document")) - xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml") xBody = xmlSubElem(xRoot, _wTag("body")) self._rels["document.xml"] = DocXXmlRel( rId=rId, diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 2d98567d..04e9cf94 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2065,12 +2065,8 @@ class GuiDocEditor(QPlainTextEdit): feature for French, Spanish, etc, so it doesn't insert a space before colons in meta data lines. See issue #1090. """ - if char == ":" and len(text) > 1: - if text[0] == "@": - return False - if text[0] == "%": - if text[1:].lstrip()[:9].lower() == "synopsis:": - return False + if char == ":" and len(text) > 1 and text[0] == "@": + return False return True def _autoSelect(self) -> QTextCursor: diff --git a/tests/reference/fmtToOdt_SaveFlat_document.fodt b/tests/reference/fmtToOdt_SaveFlat_document.fodt index 1e82c1c6..0b500e35 100644 --- a/tests/reference/fmtToOdt_SaveFlat_document.fodt +++ b/tests/reference/fmtToOdt_SaveFlat_document.fodt @@ -1,13 +1,13 @@ - + - 2024-10-29T09:33:22 + 2024-10-29T23:24:19 novelWriter/2.6a3 Jane Smith 1234 P42DT12H34M56S Test Project - 2024-10-29T09:33:22 + 2024-10-29T23:24:19 Jane Smith @@ -82,6 +82,12 @@ + + + + + + @@ -95,10 +101,30 @@ - Chapter One - Text + + + + + + + + + + + + + + My Novel + Word Count: 00 paragrphsWeb: http://example.com + Chapter One + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum. Chapter Two - Text + Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis. + 1 + + Lorem ipsum + + diff --git a/tests/reference/fmtToOdt_SaveFull_content.xml b/tests/reference/fmtToOdt_SaveFull_content.xml index 0654b0f3..4c99fd4b 100644 --- a/tests/reference/fmtToOdt_SaveFull_content.xml +++ b/tests/reference/fmtToOdt_SaveFull_content.xml @@ -1,5 +1,5 @@ - + @@ -7,13 +7,47 @@ + + + + + + - Chapter One - Text + + + + + + + + + + + + + + My Novel + + Word Count: + + 0 + + + 0 paragrphsWeb: http://example.com + + Chapter One + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum. Chapter Two - Text + Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis. + 1 + + Lorem ipsum + + + diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd index b97e07c1..fe96f10a 100644 --- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd +++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd @@ -1,8 +1,8 @@ %%~name: New Scene %%~path: 000000000000d/000000000000f %%~kind: NOVEL/DOCUMENT -%%~hash: 0566024662fb6ed7ed645717addd64dabf058915 -%%~date: 2024-10-27 13:03:13/2024-10-27 13:03:18 +%%~hash: 89b54ddaec2fddfd220e91dd438c84fb3aef9fc2 +%%~date: 2024-10-30 00:07:21/2024-10-30 00:07:26 # Novel ## Chapter @@ -40,7 +40,9 @@ Some “ double quoted text with spaces padded ”. @object: NoSpaceAdded -% synopsis: No space before this colon. +% synopsis : Space before this is OK. + +%Footnote.abc : A simple footnote. Add space before this colon : See? diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 55133ebe..66c1939d 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project Jane Doe @@ -46,7 +46,7 @@ New Chapter - + New Scene diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 8effcd2a..f4562d5b 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -1490,5 +1490,6 @@ def testCoreIndex_processComment(): # Padding with term assert processComment("%note.term: Hi") == (nwComment.NOTE, "term", "Hi", 6, 11) assert processComment("% note.term: Hi") == (nwComment.NOTE, "term", "Hi", 7, 12) + assert processComment("% note.term : Hi") == (nwComment.NOTE, "term", "Hi", 7, 13) assert processComment("% note. term : Hi") == (nwComment.PLAIN, "", "note. term : Hi", 0, 0) assert processComment("% note . term : Hi") == (nwComment.PLAIN, "", "note . term : Hi", 0, 0) diff --git a/tests/test_formats/test_fmt_todocx.py b/tests/test_formats/test_fmt_todocx.py index e4ecc793..2ae740ba 100644 --- a/tests/test_formats/test_fmt_todocx.py +++ b/tests/test_formats/test_fmt_todocx.py @@ -32,28 +32,9 @@ from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt from novelwriter.formats.shared import BlockFmt, BlockTyp -from novelwriter.formats.todocx import ToDocX, _mkTag, _wTag +from novelwriter.formats.todocx import OOXML_SCM, ToDocX, _mkTag, _wTag -from tests.tools import DOCX_IGNORE, cmpFiles - -OOXML_SCM = "http://schemas.openxmlformats.org" -XML_NS = [ - f' xmlns:r="{OOXML_SCM}/officeDocument/2006/relationships"', - f' xmlns:w="{OOXML_SCM}/wordprocessingml/2006/main"', - f' xmlns:cp="{OOXML_SCM}/package/2006/metadata/core-properties"', - ' xmlns:dc="http://purl.org/dc/elements/1.1/"', - ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', - ' xmlns:xml="http://www.w3.org/XML/1998/namespace"', - ' xmlns:dcterms="http://purl.org/dc/terms/"', -] - - -def xmlToText(xElem): - """Get the text content of an XML element.""" - rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() - for ns in XML_NS: - rTxt = rTxt.replace(ns, "") - return rTxt +from tests.tools import DOCX_IGNORE, cmpFiles, xmlToText @pytest.mark.core @@ -610,6 +591,49 @@ def testFmtToDocX_Footnotes(mockGUI): ) +@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.""" diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 4b3178b8..f4dfdb7b 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -45,7 +45,7 @@ class BareTokenizer(Tokenizer): super().doConvert() # type: ignore (deliberate check) def closeDocument(self): - super().doConvert() # type: ignore (deliberate check) + super().closeDocument() # type: ignore (deliberate check) def saveDocument(self, path) -> None: super().saveDocument(path) # type: ignore (deliberate check) @@ -60,6 +60,9 @@ def testFmtToken_Abstracts(mockGUI, tstPaths): with pytest.raises(NotImplementedError): tokens.doConvert() + with pytest.raises(NotImplementedError): + tokens.closeDocument() + with pytest.raises(NotImplementedError): tokens.saveDocument(tstPaths) diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index 536c977e..18d128d7 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -35,26 +35,7 @@ from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt from novelwriter.formats.toodt import ODTParagraphStyle, ODTTextStyle, ToOdt, XMLParagraph, _mkTag -from tests.tools import ODT_IGNORE, cmpFiles - -XML_NS = [ - ' 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"', -] - - -def xmlToText(xElem): - """Get the text content of an XML element.""" - rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() - for ns in XML_NS: - rTxt = rTxt.replace(ns, "") - return rTxt +from tests.tools import ODT_IGNORE, cmpFiles, xmlToText @pytest.mark.core @@ -237,6 +218,46 @@ def testFmtToOdt_TextFormatting(mockGUI): ) +@pytest.mark.core +def testFmtToOdt_Fields(mockGUI): + """Test formatting of footnotes.""" + project = NWProject() + odt = ToOdt(project, True) + odt.initDocument() + + # Field Builder + xNode = odt._generateField("a:allWords", 0x00) + assert isinstance(xNode, ET.Element) + assert xmlToText(xNode) == ( + '' + '0' + ) + # assert odt._usedFields == [(xNode.find(_wTag("t")), "b")] + + assert odt._generateField("a", 0x00) is None + + # Full Processing + odt._text = ( + "Word Count: [field:allWords]\n" + "Character Count: [field:allChars]\n" + "Chicken Count: [field:allChickens]\n" + ) + odt.tokenizeText() + odt.doConvert() + odt.countStats() + assert xmlToText(odt._xBody) == ( + '' + '' + 'Word Count: 0' + 'Character Count: 0' + 'Chicken Count: 0' + '' + ) + + @pytest.mark.core def testFmtToOdt_DialogueFormatting(mockGUI): """Test formatting of dialogue.""" @@ -770,7 +791,7 @@ def testFmtToOdt_ConvertDirect(mockGUI): @pytest.mark.core -def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): +def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths, ipsumText): """Test the document save functions.""" project = NWProject() project.data.setAuthor("Jane Smith") @@ -796,14 +817,20 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): assert odt._mDocRight == "1.500cm" odt._text = ( + "#! My Novel\n\n" + "**Word Count: [field:allWords]**\n" + "[field:paragraphCount] paragrphs\n" + "Web: http://example.com\n\n" "## Chapter One\n\n" - "Text\n\n" + f"{ipsumText[0]}\n\n" "## Chapter Two\n\n" - "Text\n\n" + f"{ipsumText[1]}[footnote:abc]\n\n" + "%Footnote.abc: Lorem ipsum\n\n" ) odt.tokenizeText() odt.initDocument() odt.doConvert() + odt.countStats() odt.closeDocument() flatFile = fncPath / "document.fodt" @@ -818,7 +845,7 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths): @pytest.mark.core -def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths): +def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths, ipsumText): """Test the document save functions.""" project = NWProject() project.data.setAuthor("Jane Smith") @@ -833,14 +860,20 @@ def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths): odt.setHeaderFormat(f"{nwHeadFmt.DOC_PROJECT} - {nwHeadFmt.DOC_AUTHOR}", 0) odt._text = ( + "#! My Novel\n\n" + "**Word Count: [field:allWords]**\n" + "[field:paragraphCount] paragrphs\n" + "Web: http://example.com\n\n" "## Chapter One\n\n" - "Text\n\n" + f"{ipsumText[0]}\n\n" "## Chapter Two\n\n" - "Text\n\n" + f"{ipsumText[1]}[footnote:abc]\n\n" + "%Footnote.abc: Lorem ipsum\n\n" ) odt.tokenizeText() odt.initDocument() odt.doConvert() + odt.countStats() odt.closeDocument() fullFile = fncPath / "document.odt" diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 3910e7b1..62c60859 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -2184,27 +2184,3 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): assert docEditor.textCursor().selectedText() == "" # qtbot.stop() - - -@pytest.mark.gui -def testGuiEditor_StaticMethods(): - """Test the document editor's static methods.""" - # Check the method that decides if it is allowed to insert a space - # before a colon using the French, Spanish, etc language feature - assert GuiDocEditor._allowSpaceBeforeColon("", "") is True - assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True - - assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True - - assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False - assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True - assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index f09d885f..eb87ac31 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -474,7 +474,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): # Special Formatting # ================== - # Insert spaces before colon, but ignore tags and synopsis + # Insert spaces before colon, but ignore tags docEditor._typPadBefore = ":" for c in "@object: NoSpaceAdded": @@ -482,7 +482,12 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) - for c in "% synopsis: No space before this colon.": + for c in "% synopsis: Space before this is OK.": + qtbot.keyClick(docEditor, c, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) + + for c in "%Footnote.abc: A simple footnote.": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) diff --git a/tests/tools.py b/tests/tools.py index 04d79634..504eb5b9 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -21,6 +21,7 @@ along with this program. If not, see . from __future__ import annotations import shutil +import xml.etree.ElementTree as ET from datetime import datetime from pathlib import Path @@ -110,6 +111,15 @@ def cmpFiles( return not diffFound +def xmlToText(xElem): + """Get the text content of an XML element.""" + text = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode() + bits = text.partition(">") + node = bits[0].partition(" ") + rest = " ".join(x for x in node[2].split() if not x.startswith("xmlns")).replace("/", " /") + return f"{node[0]}{rest}{bits[1]}{bits[2]}" + + def readFile(fileName: str | Path): """Returns the content of a file as a string.""" with open(fileName, mode="r", encoding="utf-8") as inFile: