diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index ed0d2e9a..e005a3ec 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -39,14 +39,16 @@ from novelwriter.constants import nwFiles logger = logging.getLogger(__name__) -FILE_VERSION = "1.4" # The current project file format version +FILE_VERSION = "1.5" # The current project file format version +HEX_VERSION = 0x0105 NUM_VERSION = { - "1.0": 0x0100, - "1.1": 0x0101, - "1.2": 0x0102, - "1.3": 0x0103, - "1.4": 0x0104, + "1.0": 0x0100, # Up to 0.7 + "1.1": 0x0101, # Up to 0.10 + "1.2": 0x0102, # Up to 1.5 + "1.3": 0x0103, # Up to 2.0 Beta 1 + "1.4": 0x0104, # Up to 2.0 RC 2 + "1.5": 0x0105, # Current } @@ -84,9 +86,15 @@ class ProjectXMLReader: 1.4 Introduces a more compact format for storing items. All settings aside from name are now attributes. This format also changes the - way satus and importance labels, last used handles, and title - formats are stored. They are now all stored as key/value sets. - Introduced in version 2.0. + way satus and importance labels are stored. This format was only + a part of version 2.0 RC 1 + + 1.5 The actual format released for 2.0. It moves last used handles + and title formats into a key/value format similar to auto- + replace, status and imporetance. It adds the heading value to + the content item meta entry. It also moves meta data related to + the project or the content into their respective section nodes + as attributes. The id attribute was also added to the project. """ def __init__(self, path): @@ -201,13 +209,13 @@ class ProjectXMLReader: self._parseProjectSettings(xSection, projData) elif xSection.tag == "content": if self._version >= 0x0104: - self._parseProjectContent(xSection, projContent) + self._parseProjectContent(xSection, projData, projContent) else: - self._parseProjectContentLegacy(xSection, projContent, projData) + self._parseProjectContentLegacy(xSection, projData, projContent) else: logger.warning("Ignored in XML", xSection.tag) - if self._version == 0x0104: + if self._version == HEX_VERSION: self._state = XMLReadState.PARSED_OK else: self._state = XMLReadState.WAS_LEGACY @@ -224,7 +232,12 @@ class ProjectXMLReader: """Parse the project section of the XML file. """ logger.debug("Parsing section") - projData.setUuid(xSection.attrib.get("id", None)) + + projData.setUuid(xSection.attrib.get("id", None)) # Added in 1.5 + projData.setSaveCount(xSection.attrib.get("saveCount", 0)) # Moved in 1.5 + projData.setAutoCount(xSection.attrib.get("autoCount", 0)) # Moved in 1.5 + projData.setEditTime(xSection.attrib.get("editTime", 0)) # Moved in 1.5 + for xItem in xSection: if xItem.tag == "name": projData.setName(xItem.text) @@ -232,15 +245,19 @@ class ProjectXMLReader: projData.setTitle(xItem.text) elif xItem.tag == "author": projData.addAuthor(xItem.text) - elif xItem.tag == "saveCount": - projData.setSaveCount(xItem.text) - elif xItem.tag == "autoCount": - projData.setAutoCount(xItem.text) - elif xItem.tag == "editTime": - projData.setEditTime(xItem.text) else: logger.warning("Ignored in XML", xItem.tag) + # Deprecated Nodes + if self._version < HEX_VERSION: + for xItem in xSection: + if xItem.tag == "saveCount": # Moved to attribute in 1.5 + projData.setSaveCount(xItem.text) + elif xItem.tag == "autoCount": # Moved to attribute in 1.5 + projData.setAutoCount(xItem.text) + elif xItem.tag == "editTime": # Moved to attribute in 1.5 + projData.setEditTime(xItem.text) + return def _parseProjectSettings(self, xSection, projData): @@ -257,10 +274,6 @@ class ProjectXMLReader: projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": projData.setSpellLang(xItem.text) - elif xItem.tag == "novelWordCount": - projData.setInitCounts(novel=xItem.text) - elif xItem.tag == "notesWordCount": - projData.setInitCounts(notes=xItem.text) elif xItem.tag == "status": self._parseStatusImport(xItem, projData.itemStatus) elif xItem.tag in ("import", "importance"): @@ -273,67 +286,80 @@ class ProjectXMLReader: else: # Pre 1.2 format projData.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": - if self._version >= 0x0104: + if self._version >= 0x0105: projData.setTitleFormat(self._parseDictKeyText(xItem)) else: # Pre 1.4 format projData.setTitleFormat(self._parseDictTagText(xItem)) else: logger.warning("Ignored in XML", xItem.tag) + # Deprecated Nodes + if self._version < HEX_VERSION: + for xItem in xSection: + if xItem.tag == "novelWordCount": # Moved to content attribute in 1.5 + projData.setInitCounts(novel=xItem.text) + elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5 + projData.setInitCounts(notes=xItem.text) + return - def _parseProjectContent(self, xSection, projContent): + def _parseProjectContent(self, xSection, projData, projContent): """Parse the content section of the XML file. """ logger.debug("Parsing section") + projData.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5 + projData.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5 + for xItem in xSection: - if xItem.tag == "item": - item = {} - meta = {} - name = {} - itemName = "" - - item["handle"] = checkStringNone(xItem.attrib.get("handle"), None) - item["parent"] = checkStringNone(xItem.attrib.get("parent"), None) - item["root"] = checkStringNone(xItem.attrib.get("root"), None) - item["order"] = checkInt(xItem.attrib.get("order"), 0) - item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE") - item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS") - item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT") - for xVal in xItem: - if xVal.tag == "meta": - meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False) - meta["heading"] = checkString(xVal.attrib.get("heading"), "H0") - meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0) - meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0) - meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0) - meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0) - elif xVal.tag == "name": - itemName = simplified(checkString(xVal.text, "")) - name["status"] = checkStringNone(xVal.attrib.get("status"), None) - name["import"] = checkStringNone(xVal.attrib.get("import"), None) - name["active"] = checkBool(xVal.attrib.get("active"), False) - - # ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases. - if "exported" in xVal.attrib: - name["active"] = checkBool(xVal.attrib.get("exported"), False) - else: - logger.warning("Ignored in XML", xVal.tag) - - projContent.append({ - "name": itemName, - "itemAttr": item, - "metaAttr": meta, - "nameAttr": name, - }) - - else: + if xItem.tag != "item": logger.warning("Ignored item in XML", xItem.tag) + continue + + item = {} + meta = {} + name = {} + itemName = "" + + item["handle"] = checkStringNone(xItem.attrib.get("handle"), None) + item["parent"] = checkStringNone(xItem.attrib.get("parent"), None) + item["root"] = checkStringNone(xItem.attrib.get("root"), None) + item["order"] = checkInt(xItem.attrib.get("order"), 0) + item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE") + item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS") + item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT") + for xVal in xItem: + if xVal.tag == "meta": + meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False) + meta["heading"] = checkString(xVal.attrib.get("heading"), "H0") + meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0) + meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0) + meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0) + meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0) + elif xVal.tag == "name": + itemName = simplified(checkString(xVal.text, "")) + name["status"] = checkStringNone(xVal.attrib.get("status"), None) + name["import"] = checkStringNone(xVal.attrib.get("import"), None) + name["active"] = checkBool(xVal.attrib.get("active"), False) + else: + logger.warning("Ignored in XML", xVal.tag) + + # Deprecated Nodes + if self._version < HEX_VERSION: + for xVal in xItem: + if xVal.tag == "name": + name["active"] = checkBool(xVal.attrib.get("exported"), name["active"]) + + projContent.append({ + "name": itemName, + "itemAttr": item, + "metaAttr": meta, + "nameAttr": name, + }) return - def _parseProjectContentLegacy(self, xSection, projContent, projData): + def _parseProjectContentLegacy(self, xSection, projData, projContent): """Parse the content section of the XML file for older versions. """ logger.debug("Parsing section (legacy format)") @@ -477,13 +503,17 @@ class ProjectXMLWriter: }) # Save Project Meta - xProject = etree.SubElement(xRoot, "project", attrib={"id": projData.uuid}) + projAttr = { + "id": projData.uuid, + "saveCount": str(projData.saveCount), + "autoCount": str(projData.autoCount), + "editTime": str(editTime), + } + + xProject = etree.SubElement(xRoot, "project", attrib=projAttr) self._packSingleValue(xProject, "name", projData.name) self._packSingleValue(xProject, "title", projData.title) self._packListValue(xProject, "author", projData.authors) - self._packSingleValue(xProject, "saveCount", projData.saveCount) - self._packSingleValue(xProject, "autoCount", projData.autoCount) - self._packSingleValue(xProject, "editTime", editTime) # Save Project Settings xSettings = etree.SubElement(xRoot, "settings") @@ -491,8 +521,6 @@ class ProjectXMLWriter: self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) self._packSingleValue(xSettings, "spellLang", projData.spellLang) - self._packSingleValue(xSettings, "novelWordCount", projData.currCounts[0]) - self._packSingleValue(xSettings, "notesWordCount", projData.currCounts[1]) self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle) self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat) @@ -507,7 +535,13 @@ class ProjectXMLWriter: self._packSingleValue(xImport, "entry", label, attrib=attrib) # Save Tree Content - xContent = etree.SubElement(xRoot, "content", attrib={"count": str(len(projContent))}) + contAttr = { + "itemCount": str(len(projContent)), + "novelWords": str(projData.currCounts[0]), + "notesWords": str(projData.currCounts[1]), + } + + xContent = etree.SubElement(xRoot, "content", attrib=contAttr) for item in projContent: xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index d7d7bde1..b1f151ac 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,16 @@ - - + + Sample Project Sample Project Jane Smith Jay Doh - 1421 - 236 - 69454 False en_GB True None - 954 - 409 636b6aa9b697b 636b6aa9b697b @@ -50,7 +45,7 @@ Main - + Novel diff --git a/tests/files/nwProject-1.4.nwx b/tests/files/nwProject-1.4.nwx index 71baddbc..85ff2f63 100644 --- a/tests/files/nwProject-1.4.nwx +++ b/tests/files/nwProject-1.4.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Sample Project Jane Smith @@ -14,25 +14,24 @@ en_GB True en_GB + 636b6aa9b697b + 636b6aa9b697b + 7031beac91f75 + 7031beac91f75 + 1363 954 409 - - 636b6aa9b697b - 636b6aa9b697b - 7031beac91f75 - 7031beac91f75 - B E D - %title% - Chapter %chw%: %title% - %title% - Scene %ch%.%sc%: %title% - + %title% + Chapter %chw%: %title% + %title% + Scene %ch%.%sc%: %title% +
New @@ -56,56 +55,56 @@ Novel
- - Title Page + + Title Page - - Page + + Page - - Part One + + Part One - - Chapter One + + Chapter One - - Making a Scene + + Making a Scene - - Another Scene + + Another Scene - - Interlude + + Interlude - - A Note on Structure + + A Note on Structure - - Chapter Two + + Chapter Two - - We Found John! + + We Found John! Sequel - - Title Page + + Title Page - - Chapter One + + Chapter One @@ -116,28 +115,28 @@ Main Characters - - John Smith + + John Smith - - Jane Smith + + Jane Smith Locations - - Earth + + Earth - - Space + + Space - - Mars + + Mars @@ -148,16 +147,16 @@ Scenes - - Old File + + Old File Trash - - Delete Me! + + Delete Me!
diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx new file mode 100644 index 00000000..28f03cef --- /dev/null +++ b/tests/files/nwProject-1.5.nwx @@ -0,0 +1,158 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + + + True + en_GB + True + en_GB + + 636b6aa9b697b + 636b6aa9b697b + 7031beac91f75 + 7031beac91f75 + + + B + E + D + + + %title% + Chapter %chw%: %title% + %title% + Scene %ch%.%sc%: %title% + + + + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished + + + None + Minor + Major + Main + + + + + + Novel + + + + Title Page + + + + Page + + + + Part One + + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + + Interlude + + + + A Note on Structure + + + + Chapter Two + + + + We Found John! + + + + Sequel + + + + Title Page + + + + Chapter One + + + + Characters + + + + Main Characters + + + + John Smith + + + + Jane Smith + + + + Locations + + + + Earth + + + + Space + + + + Mars + + + + Archive + + + + Scenes + + + + Old File + + + + Trash + + + + Delete Me! + + + diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index d78a4aa0..d5b2b2c7 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,20 +1,15 @@ - - + + Lorem Ipsum Lorem Ipsum lipsum.com - 34 - 24 - 1893 False en_GB False None - 3109 - 738 7a992350f3eb6 None @@ -45,7 +40,7 @@ Main - + Novel diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 973306cf..520883e5 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,20 +1,15 @@ - - + + New Project New Novel Jane Doe - 2 - 1 - 0 True None False None - 10 - 3 None None @@ -42,7 +37,7 @@ Main - + Novel diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 6aea2f22..6e2c26af 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,20 +1,15 @@ - - + + New Project New Novel Jane Doe - 2 - 1 - 0 True None False None - 9 - 0 None None @@ -42,7 +37,7 @@ Main - + Novel diff --git a/tests/reference/coreTools_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx index 1b083ddc..e7775cd5 100644 --- a/tests/reference/coreTools_NewCustomA_nwProject.nwx +++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx @@ -1,21 +1,16 @@ - - + + Test Custom Test Novel Jane Doe John Doh - 1 - 0 - 0 True None False None - 0 - 0 None None @@ -43,7 +38,7 @@ Main - + Novel diff --git a/tests/reference/coreTools_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx index 637afa4f..6937cb0a 100644 --- a/tests/reference/coreTools_NewCustomB_nwProject.nwx +++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx @@ -1,21 +1,16 @@ - - + + Test Custom Test Novel Jane Doe John Doh - 1 - 0 - 0 True None False None - 0 - 0 None None @@ -43,7 +38,7 @@ Main - + Novel diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx index bed1b6bf..ba168a0e 100644 --- a/tests/reference/coreTools_NewMinimal_nwProject.nwx +++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx @@ -1,19 +1,14 @@ - - + + New Project New Project - 1 - 0 - 0 True None False None - 0 - 0 None None @@ -41,7 +36,7 @@ Main - + Novel diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 2c849f63..74bae460 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,20 +1,15 @@ - - + + New Project New Novel Jane Doe - 4 - 2 - 4 True None True None - 136 - 27 000000000000f None @@ -42,7 +37,7 @@ Main - + Novel diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 84c353e8..b5fd4013 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,20 +1,15 @@ - - + + New Project New Novel Jane Doe - 2 - 1 - 0 True None False None - 9 - 0 None None @@ -42,7 +37,7 @@ Main - + Novel diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx index b557dc19..b08e3511 100644 --- a/tests/reference/projectXML_ReadLegacy10.nwx +++ b/tests/reference/projectXML_ReadLegacy10.nwx @@ -1,21 +1,16 @@ - - + + Sample Project Sample Project Jane Smith Jay Doh - 0 - 0 - 1000 True None True None - 0 - 0 None None @@ -50,7 +45,7 @@ Main - + Novel diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx index 84df9eef..c8519300 100644 --- a/tests/reference/projectXML_ReadLegacy11.nwx +++ b/tests/reference/projectXML_ReadLegacy11.nwx @@ -1,21 +1,16 @@ - - + + Sample Project Sample Project Jane Smith Jay Doh - 5 - 10 - 1000 True None True None - 0 - 0 None None @@ -50,7 +45,7 @@ Main - + Novel diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx index e87015fe..c28d7e16 100644 --- a/tests/reference/projectXML_ReadLegacy12.nwx +++ b/tests/reference/projectXML_ReadLegacy12.nwx @@ -1,21 +1,16 @@ - - + + Sample Project Sample Project Jane Smith Jay Doh - 5 - 10 - 1000 True en_GB True en_GB - 840 - 376 None None @@ -50,7 +45,7 @@ Main - + Novel diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx index a8be3ecd..8e448959 100644 --- a/tests/reference/projectXML_ReadLegacy13.nwx +++ b/tests/reference/projectXML_ReadLegacy13.nwx @@ -1,21 +1,16 @@ - - + + Sample Project Sample Project Jane Smith Jay Doh - 5 - 10 - 1000 True en_GB True en_GB - 830 - 376 None None @@ -50,7 +45,7 @@ Main - + Novel diff --git a/tests/reference/projectXML_ReadLegacy14.json b/tests/reference/projectXML_ReadLegacy14.json new file mode 100644 index 00000000..71afa3c9 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy14.json @@ -0,0 +1,677 @@ +[ + { + "name": "Novel", + "itemAttr": { + "handle": "7031beac91f75", + "parent": null, + "root": "7031beac91f75", + "order": 0, + "type": "ROOT", + "class": "NOVEL", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sc24b8f", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Title Page", + "itemAttr": { + "handle": "53b69b83cdafc", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 0, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 93, + "wordCount": 19, + "paraCount": 2, + "cursorPos": 119 + }, + "nameAttr": { + "status": "sc24b8f", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Page", + "itemAttr": { + "handle": "974e400180a99", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 1, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 251, + "wordCount": 50, + "paraCount": 2, + "cursorPos": 277 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Part One", + "itemAttr": { + "handle": "edca4be2fcaf8", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 2, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 26, + "wordCount": 6, + "paraCount": 1, + "cursorPos": 36 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Chapter One", + "itemAttr": { + "handle": "6a2d6d5f4f401", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 3, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 95, + "wordCount": 18, + "paraCount": 1, + "cursorPos": 291 + }, + "nameAttr": { + "status": "sf24ce6", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Making a Scene", + "itemAttr": { + "handle": "636b6aa9b697b", + "parent": "6a2d6d5f4f401", + "root": "7031beac91f75", + "order": 0, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 2687, + "wordCount": 479, + "paraCount": 14, + "cursorPos": 67 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Another Scene", + "itemAttr": { + "handle": "bc0cbd2a407f3", + "parent": "6a2d6d5f4f401", + "root": "7031beac91f75", + "order": 1, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 548, + "wordCount": 108, + "paraCount": 3, + "cursorPos": 465 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Interlude", + "itemAttr": { + "handle": "ba8a28a246524", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 4, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 617, + "wordCount": 101, + "paraCount": 3, + "cursorPos": 310 + }, + "nameAttr": { + "status": "s78ea90", + "import": "ia857f0", + "active": true + } + }, + { + "name": "A Note on Structure", + "itemAttr": { + "handle": "96b68994dfa3d", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 5, + "type": "FILE", + "class": "NOVEL", + "layout": "NOTE" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 1909, + "wordCount": 346, + "paraCount": 7, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf24ce6", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Chapter Two", + "itemAttr": { + "handle": "88706ddc78b1b", + "parent": "7031beac91f75", + "root": "7031beac91f75", + "order": 6, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 139, + "wordCount": 28, + "paraCount": 1, + "cursorPos": 188 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "We Found John!", + "itemAttr": { + "handle": "ae7339df26ded", + "parent": "88706ddc78b1b", + "root": "7031beac91f75", + "order": 0, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 189, + "wordCount": 37, + "paraCount": 1, + "cursorPos": 0 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Sequel", + "itemAttr": { + "handle": "e5e47ebf63b1c", + "parent": null, + "root": "e5e47ebf63b1c", + "order": 1, + "type": "ROOT", + "class": "NOVEL", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Title Page", + "itemAttr": { + "handle": "bacb7059e3083", + "parent": "e5e47ebf63b1c", + "root": "e5e47ebf63b1c", + "order": 0, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 27, + "wordCount": 5, + "paraCount": 1, + "cursorPos": 100 + }, + "nameAttr": { + "status": "sc24b8f", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Chapter One", + "itemAttr": { + "handle": "a520879ca0b45", + "parent": "e5e47ebf63b1c", + "root": "e5e47ebf63b1c", + "order": 1, + "type": "FILE", + "class": "NOVEL", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 299, + "wordCount": 55, + "paraCount": 2, + "cursorPos": 104 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Characters", + "itemAttr": { + "handle": "f6622b4617424", + "parent": null, + "root": "f6622b4617424", + "order": 2, + "type": "ROOT", + "class": "CHARACTER", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Main Characters", + "itemAttr": { + "handle": "f7e2d9f330615", + "parent": "f6622b4617424", + "root": "f6622b4617424", + "order": 0, + "type": "FOLDER", + "class": "CHARACTER", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "John Smith", + "itemAttr": { + "handle": "14298de4d9524", + "parent": "f7e2d9f330615", + "root": "f6622b4617424", + "order": 0, + "type": "FILE", + "class": "CHARACTER", + "layout": "NOTE" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 49, + "wordCount": 9, + "paraCount": 1, + "cursorPos": 24 + }, + "nameAttr": { + "status": "sf12341", + "import": "icfb3a5", + "active": true + } + }, + { + "name": "Jane Smith", + "itemAttr": { + "handle": "bb2c23b3c42cc", + "parent": "f7e2d9f330615", + "root": "f6622b4617424", + "order": 1, + "type": "FILE", + "class": "CHARACTER", + "layout": "NOTE" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 55, + "wordCount": 9, + "paraCount": 1, + "cursorPos": 25 + }, + "nameAttr": { + "status": "sf12341", + "import": "i2d7a54", + "active": true + } + }, + { + "name": "Locations", + "itemAttr": { + "handle": "15c4492bd5107", + "parent": null, + "root": "15c4492bd5107", + "order": 3, + "type": "ROOT", + "class": "WORLD", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Earth", + "itemAttr": { + "handle": "b3e74dbc1f584", + "parent": "15c4492bd5107", + "root": "15c4492bd5107", + "order": 0, + "type": "FILE", + "class": "WORLD", + "layout": "NOTE" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 76, + "wordCount": 15, + "paraCount": 1, + "cursorPos": 20 + }, + "nameAttr": { + "status": "sf12341", + "import": "i56be10", + "active": true + } + }, + { + "name": "Space", + "itemAttr": { + "handle": "f1471bef9f2ae", + "parent": "15c4492bd5107", + "root": "15c4492bd5107", + "order": 1, + "type": "FILE", + "class": "WORLD", + "layout": "NOTE" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 115, + "wordCount": 24, + "paraCount": 1, + "cursorPos": 133 + }, + "nameAttr": { + "status": "sf12341", + "import": "icfb3a5", + "active": true + } + }, + { + "name": "Mars", + "itemAttr": { + "handle": "5eaea4e8cdee8", + "parent": "15c4492bd5107", + "root": "15c4492bd5107", + "order": 2, + "type": "FILE", + "class": "WORLD", + "layout": "NOTE" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 28, + "wordCount": 6, + "paraCount": 1, + "cursorPos": 45 + }, + "nameAttr": { + "status": "sf12341", + "import": "i2d7a54", + "active": true + } + }, + { + "name": "Archive", + "itemAttr": { + "handle": "6827118336ac1", + "parent": null, + "root": "6827118336ac1", + "order": 4, + "type": "ROOT", + "class": "ARCHIVE", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Scenes", + "itemAttr": { + "handle": "ae9bf3c3ea159", + "parent": "6827118336ac1", + "root": "6827118336ac1", + "order": 0, + "type": "FOLDER", + "class": "ARCHIVE", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Old File", + "itemAttr": { + "handle": "8a5deb88c0e97", + "parent": "ae9bf3c3ea159", + "root": "6827118336ac1", + "order": 0, + "type": "FILE", + "class": "ARCHIVE", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 232, + "wordCount": 42, + "paraCount": 1, + "cursorPos": 239 + }, + "nameAttr": { + "status": "s90e6c9", + "import": "ia857f0", + "active": true + } + }, + { + "name": "Trash", + "itemAttr": { + "handle": "98acd8c76c93a", + "parent": null, + "root": "98acd8c76c93a", + "order": 5, + "type": "ROOT", + "class": "TRASH", + "layout": "NO_LAYOUT" + }, + "metaAttr": { + "expanded": true, + "heading": "H0", + "charCount": 0, + "wordCount": 0, + "paraCount": 0, + "cursorPos": 0 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": false + } + }, + { + "name": "Delete Me!", + "itemAttr": { + "handle": "b8136a5a774a0", + "parent": "98acd8c76c93a", + "root": "98acd8c76c93a", + "order": 0, + "type": "FILE", + "class": "TRASH", + "layout": "DOCUMENT" + }, + "metaAttr": { + "expanded": false, + "heading": "H0", + "charCount": 30, + "wordCount": 6, + "paraCount": 1, + "cursorPos": 36 + }, + "nameAttr": { + "status": "sf12341", + "import": "ia857f0", + "active": true + } + } +] diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx new file mode 100644 index 00000000..445817a3 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy14.nwx @@ -0,0 +1,158 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + + + True + en_GB + True + en_GB + + None + None + None + None + + + B + E + D + + + %title% + Chapter %chw%: %title% + %title% + Scene %ch%.%sc%: %title% + + + + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished + + + None + Minor + Major + Main + + + + + + Novel + + + + Title Page + + + + Page + + + + Part One + + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + + Interlude + + + + A Note on Structure + + + + Chapter Two + + + + We Found John! + + + + Sequel + + + + Title Page + + + + Chapter One + + + + Characters + + + + Main Characters + + + + John Smith + + + + Jane Smith + + + + Locations + + + + Earth + + + + Space + + + + Mars + + + + Archive + + + + Scenes + + + + Old File + + + + Trash + + + + Delete Me! + + + diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index b3af7289..3bbc9e3a 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -42,10 +42,10 @@ class MockProject: def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): """Test reading the current XML file format. """ - refFile = tstPaths.filesDir / "nwProject-1.4.nwx" + refFile = tstPaths.filesDir / "nwProject-1.5.nwx" tstFile = tstPaths.outDir / "ProjectXML_ReadCurrent.nwx" - xmlFile = fncPath / "nwProject-1.4.nwx" - bakFile = fncPath / "nwProject-1.4.bak" + xmlFile = fncPath / "nwProject-1.5.nwx" + bakFile = fncPath / "nwProject-1.5.bak" outFile = fncPath / "nwProject.nwx" xmlReader = ProjectXMLReader(xmlFile) @@ -81,7 +81,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): # Check parsing of unkown sections writeFile(xmlFile, ( - "" + "" " " " " " " @@ -129,7 +129,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): assert xmlReader.read(data, content) is True assert xmlReader.state == XMLReadState.PARSED_OK assert xmlReader.xmlRoot == "novelWriterXML" - assert xmlReader.xmlVersion == 0x0104 + assert xmlReader.xmlVersion == 0x0105 assert xmlReader.appVersion == "2.0-rc1" assert xmlReader.hexVersion == "0x020000c1" @@ -809,3 +809,150 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd): assert cmpFiles(testFile, compFile) # END Test testCoreProjectXML_ReadLegacy13 + + +@pytest.mark.core +def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd): + """Test reading the version 1.4 XML file format. + """ + refFile = tstPaths.filesDir / "nwProject-1.4.nwx" + xmlFile = fncPath / "nwProject-1.4.nwx" + outFile = fncPath / "nwProject.nwx" + copyfile(refFile, xmlFile) + + xmlReader = ProjectXMLReader(xmlFile) + assert xmlReader.state == XMLReadState.NO_ACTION + + data = NWProjectData(MockProject()) + content = [] + + assert xmlReader.read(data, content) is True + assert xmlReader.state == XMLReadState.WAS_LEGACY + assert xmlReader.xmlRoot == "novelWriterXML" + assert xmlReader.xmlVersion == 0x0104 + assert xmlReader.appVersion == "2.0-rc1" + assert xmlReader.hexVersion == "0x020000c1" + + # Check loaded data + assert data.name == "Sample Project" + assert data.title == "Sample Project" + assert data.authors == ["Jane Smith", "Jay Doh"] + assert data.saveCount == 5 + assert data.autoCount == 10 + assert data.editTime == 1000 + + assert data.doBackup is True + assert data.language == "en_GB" + assert data.spellCheck is True + assert data.spellLang == "en_GB" + assert data.initCounts == (954, 409) + assert data.currCounts == (954, 409) + + assert data.getLastHandle("editor") is None # Dropped by conversion + assert data.getLastHandle("viewer") is None # Dropped by conversion + assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.3 + assert data.getLastHandle("outline") is None # Doesn't exist in 1.3 + + assert data.getTitleFormat("title") == "%title%" + assert data.getTitleFormat("chapter") == "Chapter %chw%: %title%" + assert data.getTitleFormat("unnumbered") == "%title%" + assert data.getTitleFormat("scene") == "Scene %ch%.%sc%: %title%" + assert data.getTitleFormat("section") == "" + + assert data.itemStatus.name("sf12341") == "New" + assert data.itemStatus.name("sf24ce6") == "Notes" + assert data.itemStatus.name("sc24b8f") == "Started" + assert data.itemStatus.name("s90e6c9") == "1st Draft" + assert data.itemStatus.name("sd51c5b") == "2nd Draft" + assert data.itemStatus.name("s8ae72a") == "3rd Draft" + assert data.itemStatus.name("s78ea90") == "Finished" + + assert data.itemImport.name("ia857f0") == "None" + assert data.itemImport.name("icfb3a5") == "Minor" + assert data.itemImport.name("i2d7a54") == "Major" + assert data.itemImport.name("i56be10") == "Main" + + assert data.itemStatus.cols("sf12341") == (100, 100, 100) + assert data.itemStatus.cols("sf24ce6") == (200, 50, 0) + assert data.itemStatus.cols("sc24b8f") == (182, 60, 0) + assert data.itemStatus.cols("s90e6c9") == (193, 129, 0) + assert data.itemStatus.cols("sd51c5b") == (193, 129, 0) + assert data.itemStatus.cols("s8ae72a") == (193, 129, 0) + assert data.itemStatus.cols("s78ea90") == (58, 180, 58) + + assert data.itemImport.cols("ia857f0") == (100, 100, 100) + assert data.itemImport.cols("icfb3a5") == (0, 122, 188) + assert data.itemImport.cols("i2d7a54") == (21, 0, 180) + assert data.itemImport.cols("i56be10") == (117, 0, 175) + + assert data.itemStatus.count("sf12341") == 4 + assert data.itemStatus.count("sf24ce6") == 2 + assert data.itemStatus.count("sc24b8f") == 3 + assert data.itemStatus.count("s90e6c9") == 7 + assert data.itemStatus.count("sd51c5b") == 0 + assert data.itemStatus.count("s8ae72a") == 0 + assert data.itemStatus.count("s78ea90") == 1 + + assert data.itemImport.count("ia857f0") == 5 + assert data.itemImport.count("icfb3a5") == 2 + assert data.itemImport.count("i2d7a54") == 2 + assert data.itemImport.count("i56be10") == 1 + + # Compare content + dumpFile = tstPaths.outDir / "projectXML_ReadLegacy14.json" + compFile = tstPaths.refDir / "projectXML_ReadLegacy14.json" + with open(dumpFile, mode="w", encoding="utf-8") as dump: + json.dump(content, dump, indent=2) + assert cmpFiles(dumpFile, compFile) + + packedContent = [] + mockProject = MockProject() + mockProject.__setattr__("data", data) + status = {} + for entry in content: + item = NWItem(mockProject) + item.unpack(entry) + status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] + packedContent.append(item.pack()) + + assert status == { + "7031beac91f75": "Started", + "53b69b83cdafc": "Started", + "974e400180a99": "New", + "edca4be2fcaf8": "1st Draft", + "6a2d6d5f4f401": "Notes", + "636b6aa9b697b": "1st Draft", + "bc0cbd2a407f3": "1st Draft", + "ba8a28a246524": "Finished", + "96b68994dfa3d": "Notes", + "88706ddc78b1b": "1st Draft", + "ae7339df26ded": "1st Draft", + "e5e47ebf63b1c": "New", + "bacb7059e3083": "Started", + "a520879ca0b45": "1st Draft", + "f6622b4617424": "None", + "f7e2d9f330615": "None", + "14298de4d9524": "Minor", + "bb2c23b3c42cc": "Major", + "15c4492bd5107": "None", + "b3e74dbc1f584": "Main", + "f1471bef9f2ae": "Minor", + "5eaea4e8cdee8": "Major", + "6827118336ac1": "New", + "ae9bf3c3ea159": "New", + "8a5deb88c0e97": "1st Draft", + "98acd8c76c93a": "None", + "b8136a5a774a0": "None", + } + + # Save the project again, which should produce an identical project xml + timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) + xmlWriter = ProjectXMLWriter(fncPath) + data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") + assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True + testFile = tstPaths.outDir / "projectXML_ReadLegacy14.nwx" + compFile = tstPaths.refDir / "projectXML_ReadLegacy14.nwx" + copyfile(outFile, testFile) + assert cmpFiles(testFile, compFile) + +# END Test testCoreProjectXML_ReadLegacy14 diff --git a/tests/tools.py b/tests/tools.py index 7824268b..e101bcbb 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -25,7 +25,7 @@ import shutil from PyQt5.QtWidgets import qApp -XML_IGNORE = ("