From ba88160debe563ec0e0f9cb4e7bc1ca3966fe607 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 29 Sep 2022 20:07:52 +0200
Subject: [PATCH 01/18] Rough framework for XML reader/writer classes
---
novelwriter/core/projectxml.py | 104 +++++++++++++++++++++++++++++++++
1 file changed, 104 insertions(+)
create mode 100644 novelwriter/core/projectxml.py
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
new file mode 100644
index 00000000..b47a6181
--- /dev/null
+++ b/novelwriter/core/projectxml.py
@@ -0,0 +1,104 @@
+"""
+novelWriter – Project XML Read/Write
+====================================
+Classes for reading and writing the project XML file
+
+File History:
+Created: 2022-09-28 [1.7.b1]
+
+This file is a part of novelWriter
+Copyright 2018–2022, Veronica Berglyd Olsen
+
+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 .
+"""
+
+import os
+import logging
+
+from enum import Enum
+from lxml import etree
+
+logger = logging.getLogger(__name__)
+
+
+class XMLReadState(Enum):
+
+ NO_ACTION = 0
+ NO_ERROR = 1
+ PARSED_BACKUP = 2
+ CANNOT_PARSE = 3
+
+# END Class XMLReadState
+
+
+class ProjectXMLReader:
+
+ def __init__(self, path):
+
+ self._path = path
+ self._state = XMLReadState.NO_ACTION
+
+ return
+
+ def read(self):
+ """
+ """
+ try:
+ xml = etree.parse(self._path)
+ self._state = XMLReadState.NO_ERROR
+
+ except Exception as exc:
+ # Trying to open backup file instead
+ logger.error("Failed to parse project xml", exc_info=exc)
+ self._state = XMLReadState.CANNOT_PARSE
+
+ backFile = self._path[:-3]+"bak"
+ if os.path.isfile(backFile):
+ try:
+ xml = etree.parse(backFile)
+ self._state = XMLReadState.PARSED_BACKUP
+ logger.info("Backup project file parsed")
+ except Exception as exc:
+ logger.error("Failed to parse backup project xml", exc_info=exc)
+ self._state = XMLReadState.CANNOT_PARSE
+ return False
+ else:
+ return False
+
+ return True
+
+ ##
+ # Internal Functions
+ ##
+
+# END Class ProjectXMLReader
+
+
+class ProjectXMLWriter:
+
+ def __init__(self, path):
+
+ self._path = path
+ self._error = None
+
+ return
+
+ def write(self):
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+# END Class ProjectXMLWriter
From ea80fd3c719ce6ec6b4eacc2d7f0bccdca1fbd26 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 30 Oct 2022 22:51:31 +0100
Subject: [PATCH 02/18] Complete the XML parsing
---
novelwriter/core/item.py | 107 ++++-------
novelwriter/core/project.py | 211 +++++++--------------
novelwriter/core/projectxml.py | 331 ++++++++++++++++++++++++++++++++-
novelwriter/core/status.py | 25 +--
novelwriter/core/tree.py | 14 +-
5 files changed, 447 insertions(+), 241 deletions(-)
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 40426d67..66cb7fbe 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -148,7 +148,7 @@ class NWItem:
return self._cursorPos
##
- # XML Pack/Unpack
+ # Pack/Unpack Data
##
def packXML(self, xParent):
@@ -167,7 +167,7 @@ class NWItem:
metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE:
- metaAttrib["mainHeading"] = str(self._heading)
+ metaAttrib["heading"] = str(self._heading)
metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount)
@@ -185,70 +185,31 @@ class NWItem:
return
- def unpackXML(self, xItem):
- """Set the values from an XML entry of type 'item'.
+ def unpack(self, data):
+ """Set the values from a data dictionary.
"""
- if xItem.tag != "item":
- logger.error("XML entry is not an NWItem")
- return False
-
- if "handle" in xItem.attrib:
- self.setHandle(xItem.attrib["handle"])
+ if "handle" in data:
+ self.setHandle(data["handle"])
else:
logger.error("XML item entry does not have a handle")
return False
- self.setParent(xItem.attrib.get("parent", None))
- self.setRoot(xItem.attrib.get("root", None))
- self.setOrder(xItem.attrib.get("order", 0))
- self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
- self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
- self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
-
- for xValue in xItem:
- if xValue.tag == "meta":
- self.setExpanded(xValue.attrib.get("expanded", False))
- self.setMainHeading(xValue.attrib.get("mainHeading", "H0"))
- self.setCharCount(xValue.attrib.get("charCount", 0))
- self.setWordCount(xValue.attrib.get("wordCount", 0))
- self.setParaCount(xValue.attrib.get("paraCount", 0))
- self.setCursorPos(xValue.attrib.get("cursorPos", 0))
- elif xValue.tag == "name":
- self.setName(xValue.text)
- self.setStatus(xValue.attrib.get("status", None))
- self.setImport(xValue.attrib.get("import", None))
- self.setActive(xValue.attrib.get("active", True))
-
- # ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
- if "exported" in xValue.attrib:
- self.setActive(xValue.attrib.get("exported", True))
-
- # Legacy Format (1.3 and earlier)
- elif xValue.tag == "status":
- self.setImportStatus(xValue.text)
- elif xValue.tag == "type":
- self.setType(xValue.text)
- elif xValue.tag == "class":
- self.setClass(xValue.text)
- elif xValue.tag == "layout":
- self.setLayout(xValue.text)
- elif xValue.tag == "expanded":
- self.setExpanded(xValue.text)
- elif xValue.tag == "exported":
- self.setActive(xValue.text)
- elif xValue.tag == "charCount":
- self.setCharCount(xValue.text)
- elif xValue.tag == "wordCount":
- self.setWordCount(xValue.text)
- elif xValue.tag == "paraCount":
- self.setParaCount(xValue.text)
- elif xValue.tag == "cursorPos":
- self.setCursorPos(xValue.text)
- else:
- # Sliently skip as we may otherwise cause orphaned
- # items if an otherwise valid file is opened by a
- # version of novelWriter that doesn't know the tag
- logger.error("Unknown tag '%s'", xValue.tag)
+ self.setParent(data.get("parent", None))
+ self.setRoot(data.get("root", None))
+ self.setOrder(data.get("order", 0))
+ self.setType(data.get("type", nwItemType.NO_TYPE))
+ self.setClass(data.get("class", nwItemClass.NO_CLASS))
+ self.setLayout(data.get("layout", nwItemLayout.NO_LAYOUT))
+ self.setExpanded(data.get("expanded", False))
+ self.setMainHeading(data.get("mainHeading", "H0"))
+ self.setCharCount(data.get("charCount", 0))
+ self.setWordCount(data.get("wordCount", 0))
+ self.setParaCount(data.get("paraCount", 0))
+ self.setCursorPos(data.get("cursorPos", 0))
+ self.setName(data.get("label", ""))
+ self.setStatus(data.get("status", None))
+ self.setImport(data.get("import", None))
+ self.setActive(data.get("active", True))
# Make some checks to ensure consistency
if self._type == nwItemType.ROOT:
@@ -451,8 +412,6 @@ class NWItem:
self._type = value
elif isItemType(value):
self._type = nwItemType[value]
- elif value == "TRASH":
- self._type = nwItemType.ROOT
else:
logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE
@@ -479,8 +438,6 @@ class NWItem:
self._layout = value
elif isItemLayout(value):
self._layout = nwItemLayout[value]
- elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
- self._layout = nwItemLayout.DOCUMENT
else:
logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT
@@ -532,25 +489,37 @@ class NWItem:
def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
- self._charCount = max(0, checkInt(count, 0))
+ if isinstance(count, int):
+ self._charCount = max(0, count)
+ else:
+ self._charCount = 0
return
def setWordCount(self, count):
"""Set the word count, and ensure that it is an integer.
"""
- self._wordCount = max(0, checkInt(count, 0))
+ if isinstance(count, int):
+ self._wordCount = max(0, count)
+ else:
+ self._wordCount = 0
return
def setParaCount(self, count):
"""Set the paragraph count, and ensure that it is an integer.
"""
- self._paraCount = max(0, checkInt(count, 0))
+ if isinstance(count, int):
+ self._paraCount = max(0, count)
+ else:
+ self._paraCount = 0
return
def setCursorPos(self, position):
"""Set the cursor position, and ensure that it is an integer.
"""
- self._cursorPos = max(0, checkInt(position, 0))
+ if isinstance(position, int):
+ self._cursorPos = max(0, position)
+ else:
+ self._cursorPos = 0
return
def saveInitialCount(self):
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 66f9617b..76294a08 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -35,18 +35,19 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication
+from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
+from novelwriter.error import logException
+from novelwriter.common import (
+ checkString, checkStringNone, isHandle, formatTimeStamp,
+ makeFileNameSafe, hexToInt, minmax, simplified
+)
from novelwriter.core.tree import NWTree
from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
from novelwriter.core.status import NWStatus
from novelwriter.core.options import OptionState
from novelwriter.core.document import NWDoc
-from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
-from novelwriter.error import logException
-from novelwriter.common import (
- checkString, checkBool, checkInt, checkStringNone, isHandle, formatTimeStamp,
- makeFileNameSafe, hexToInt, minmax, simplified
-)
+from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState
from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__)
@@ -62,6 +63,8 @@ class NWProject:
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
+ self._data = {}
+
# Core Elements
self._optState = OptionState(self) # Project-specific GUI options
self._projTree = NWTree(self) # The project tree
@@ -477,80 +480,45 @@ class NWProject:
# Open The Project XML File
# =========================
- try:
- nwXML = etree.parse(fileName)
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Failed to parse project xml."
- ), nwAlert.ERROR, exception=exc)
+ xmlReader = ProjectXMLReader(fileName)
+ xmlParsed = xmlReader.read()
+ xmlData = xmlReader.data
- # Trying to open backup file instead
- backFile = fileName[:-3]+"bak"
- if os.path.isfile(backFile):
+ print(json.dumps(xmlData, indent=2))
+
+ nwxRoot = xmlData.get("xmlRoot", "")
+ appVersion = xmlData.get("appVersion", self.tr("Unknown"))
+ hexVersion = xmlData.get("hexVersion", 0x0000)
+ xmlVersion = xmlData.get("xmlVersion", self.tr("Unknown"))
+
+ if not xmlParsed:
+ if xmlReader.state == XMLReadState.NOT_NWX_FILE:
self.mainGui.makeAlert(self.tr(
- "Attempting to open backup project file instead."
- ), nwAlert.INFO)
- try:
- nwXML = etree.parse(backFile)
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Failed to parse project xml."
- ), nwAlert.ERROR, exception=exc)
- self.clearProject()
- return False
+ "Project file does not appear to be a novelWriterXML file."
+ ), nwAlert.ERROR)
+ elif xmlReader.state == XMLReadState.UNKNOWN_VERSION:
+ self.mainGui.makeAlert(self.tr(
+ "Unknown or unsupported novelWriter project file format. "
+ "The project cannot be opened by this version of novelWriter. "
+ "The file was saved with novelWriter version {0}."
+ ).format(appVersion), nwAlert.ERROR)
else:
- self.clearProject()
- return False
+ self.mainGui.makeAlert(self.tr(
+ "Failed to parse project xml."
+ ), nwAlert.ERROR)
- xRoot = nwXML.getroot()
- nwxRoot = xRoot.tag
+ self.clearProject()
+ return False
- appVersion = xRoot.attrib.get("appVersion", self.tr("Unknown"))
- hexVersion = xRoot.attrib.get("hexVersion", "0x0")
- fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown"))
+ self._data = xmlData
logger.debug("XML root is '%s'", nwxRoot)
- logger.debug("File version is '%s'", fileVersion)
+ logger.debug("File version is '%s'", xmlVersion)
- # Check File Type
- # ===============
+ # Check Legacy Upgrade
+ # ====================
- if nwxRoot != "novelWriterXML":
- self.mainGui.makeAlert(self.tr(
- "Project file does not appear to be a novelWriterXML file."
- ), nwAlert.ERROR)
- self.clearProject()
- return False
-
- # Check Project Storage Version
- # =============================
-
- # Changes:
- # 1.0 : Original file format.
- # 1.1 : Changes the way documents are structured in the project
- # folder from data_X, where X is the first hex value of
- # the handle, to a single content folder.
- # 1.2 : Changes the way autoReplace entries are stored. The 1.1
- # parser will lose the autoReplace settings if allowed to
- # read the file. Introduced in version 0.10.
- # 1.3 : Reduces the number of layouts to only two. One for novel
- # documents and one for project notes. Introduced in
- # version 1.5.
- # 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 are
- # stored and handled. Introduced in version 1.7.
-
- if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"):
- self.mainGui.makeAlert(self.tr(
- "Unknown or unsupported novelWriter project file format. "
- "The project cannot be opened by this version of novelWriter. "
- "The file was saved with novelWriter version {0}."
- ).format(appVersion), nwAlert.ERROR)
- self.clearProject()
- return False
-
- if fileVersion != self.FILE_VERSION:
+ if xmlReader.state == XMLReadState.WAS_LEGACY:
msgYes = self.mainGui.askQuestion(
self.tr("File Version"),
self.tr(
@@ -581,79 +549,40 @@ class NWProject:
self.clearProject()
return False
- # Start Parsing the XML
- # =====================
+ # Extract Data
+ # ============
- for xChild in xRoot:
- if xChild.tag == "project":
- logger.debug("Found project meta")
- for xItem in xChild:
- if xItem.text is None:
- continue
- if xItem.tag == "name":
- self.projName = simplified(checkString(xItem.text, ""))
- logger.info("Project Name: '%s'", self.projName)
- elif xItem.tag == "title":
- self.bookTitle = simplified(checkString(xItem.text, ""))
- logger.info("Project Title: '%s'", self.bookTitle)
- elif xItem.tag == "author":
- author = simplified(checkString(xItem.text, ""))
- if author:
- self.bookAuthors.append(author)
- logger.debug("Author: '%s'", author)
- elif xItem.tag == "saveCount":
- self.saveCount = checkInt(xItem.text, 0)
- elif xItem.tag == "autoCount":
- self.autoCount = checkInt(xItem.text, 0)
- elif xItem.tag == "editTime":
- self.editTime = checkInt(xItem.text, 0)
+ xmlProject = xmlData.get("project", {})
- elif xChild.tag == "settings":
- logger.debug("Found project settings")
- for xItem in xChild:
- if xItem.text is None:
- continue
- if xItem.tag == "doBackup":
- self.doBackup = checkBool(xItem.text, False)
- elif xItem.tag == "language":
- self.projLang = checkStringNone(xItem.text, None)
- elif xItem.tag == "spellCheck":
- self.spellCheck = checkBool(xItem.text, False)
- elif xItem.tag == "spellLang":
- self.projSpell = checkStringNone(xItem.text, None)
- elif xItem.tag == "lastEdited":
- self.lastEdited = checkStringNone(xItem.text, None)
- elif xItem.tag == "lastViewed":
- self.lastViewed = checkStringNone(xItem.text, None)
- elif xItem.tag == "lastNovel":
- self.lastNovel = checkStringNone(xItem.text, None)
- elif xItem.tag == "lastOutline":
- self.lastOutline = checkStringNone(xItem.text, None)
- elif xItem.tag == "lastWordCount":
- self.lastWCount = checkInt(xItem.text, 0)
- elif xItem.tag == "novelWordCount":
- self.lastNovelWC = checkInt(xItem.text, 0)
- elif xItem.tag == "notesWordCount":
- self.lastNotesWC = checkInt(xItem.text, 0)
- elif xItem.tag == "status":
- self.statusItems.unpackXML(xItem)
- elif xItem.tag == "importance":
- self.importItems.unpackXML(xItem)
- elif xItem.tag == "autoReplace":
- for xEntry in xItem:
- if xEntry.tag == "entry" and "key" in xEntry.attrib:
- self.autoReplace[xEntry.attrib["key"]] = checkString(
- xEntry.text, "ERROR"
- )
- elif xItem.tag == "titleFormat":
- titleFormat = self.titleFormat.copy()
- for xEntry in xItem:
- titleFormat[xEntry.tag] = checkString(xEntry.text, "")
- self.setTitleFormat(titleFormat)
+ self.projName = xmlProject.get("name", "")
+ self.bookTitle = xmlProject.get("title", "")
+ self.bookAuthors = xmlProject.get("authors", [])
+ self.saveCount = xmlProject.get("saveCount", 0)
+ self.autoCount = xmlProject.get("autoCount", 0)
+ self.editTime = xmlProject.get("editTime", 0)
- elif xChild.tag == "content":
- logger.debug("Found project content")
- self._projTree.unpackXML(xChild)
+ logger.info("Project Name: '%s'", self.projName)
+ logger.info("Project Title: '%s'", self.bookTitle)
+
+ xmlSettings = xmlData.get("settings", {})
+
+ self.doBackup = xmlSettings.get("doBackup", False)
+ self.projLang = xmlSettings.get("language", None)
+ self.spellCheck = xmlSettings.get("spellCheck", False)
+ self.projSpell = xmlSettings.get("spellLang", None)
+ self.lastEdited = xmlSettings.get("lastEdited", None)
+ self.lastViewed = xmlSettings.get("lastViewed", None)
+ self.lastNovel = xmlSettings.get("lastNovel", None)
+ self.lastOutline = xmlSettings.get("lastOutline", None)
+ self.lastWCount = xmlSettings.get("lastWordCount", 0)
+ self.lastNovelWC = xmlSettings.get("novelWordCount", 0)
+ self.lastNotesWC = xmlSettings.get("notesWordCount", 0)
+ self.statusItems.unpack(xmlSettings.get("status", {}))
+ self.importItems.unpack(xmlSettings.get("import", {}))
+ self.autoReplace = xmlSettings.get("autoReplace", {})
+ self.titleFormat.update(xmlSettings.get("titleFormat", {}))
+
+ self._projTree.unpack(xmlData.get("content", []))
self._optState.loadSettings()
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index b47a6181..2aa2d248 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -29,15 +29,33 @@ import logging
from enum import Enum
from lxml import etree
+from novelwriter.common import (
+ checkBool, checkInt, checkStringNone, minmax, simplified, checkString
+)
+
logger = logging.getLogger(__name__)
+NUM_VERSION = {
+ "1.0": 0x0100,
+ "1.1": 0x0101,
+ "1.2": 0x0102,
+ "1.3": 0x0103,
+ "1.4": 0x0104,
+}
+
+
class XMLReadState(Enum):
- NO_ACTION = 0
- NO_ERROR = 1
- PARSED_BACKUP = 2
- CANNOT_PARSE = 3
+ NO_ACTION = 0
+ NO_ERROR = 1
+ PARSED_BACKUP = 2
+ CANNOT_PARSE = 3
+ NOT_NWX_FILE = 4
+ UNKNOWN_VERSION = 5
+ PARSING_ERROR = 6
+ PARSED_OK = 7
+ WAS_LEGACY = 8
# END Class XMLReadState
@@ -49,11 +67,34 @@ class ProjectXMLReader:
self._path = path
self._state = XMLReadState.NO_ACTION
+ self._data = {}
+ self._version = 0x0000
+ self._statusData = {}
+ self._statusMap = {}
+
return
+ ##
+ # Properties
+ ##
+
+ @property
+ def data(self):
+ return self._data
+
+ @property
+ def state(self):
+ return self._state
+
+ ##
+ # Methods
+ ##
+
def read(self):
+ """Read and parse the project XML file.
"""
- """
+ self._data = {}
+
try:
xml = etree.parse(self._path)
self._state = XMLReadState.NO_ERROR
@@ -76,12 +117,292 @@ class ProjectXMLReader:
else:
return False
+ xRoot = xml.getroot()
+ self._data["xmlRoot"] = str(xRoot.tag)
+ if xRoot.tag != "novelWriterXML":
+ self._state = XMLReadState.NOT_NWX_FILE
+ return False
+
+ # Changes:
+ # 1.0 : Original file format.
+ # 1.1 : Changes the way documents are structured in the project
+ # folder from data_X, where X is the first hex value of
+ # the handle, to a single content folder.
+ # 1.2 : Changes the way autoReplace entries are stored. The 1.1
+ # parser will lose the autoReplace settings if allowed to
+ # read the file. Introduced in version 0.10.
+ # 1.3 : Reduces the number of layouts to only two. One for novel
+ # documents and one for project notes. Introduced in
+ # version 1.5.
+ # 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 are
+ # stored and handled. Introduced in version 1.7.
+
+ fileVersion = str(xRoot.attrib.get("fileVersion", ""))
+ if fileVersion in NUM_VERSION:
+ self._version = NUM_VERSION[fileVersion]
+ else:
+ self._state = XMLReadState.UNKNOWN_VERSION
+ return False
+
+ self._data["xmlVersion"] = self._version
+ self._data["appVersion"] = str(xRoot.attrib.get("appVersion", ""))
+ self._data["hexVersion"] = str(xRoot.attrib.get("appVersion", ""))
+ self._data["timeStamp"] = str(xRoot.attrib.get("timeStamp", ""))
+
+ status = True
+ for xSection in xRoot:
+ if xSection.tag == "project":
+ status &= self._parseProjectMeta(xSection)
+ elif xSection.tag == "settings":
+ status &= self._parseProjectSettings(xSection)
+ elif xSection.tag == "content":
+ if self._version >= 0x0104:
+ status &= self._parseProjectContent(xSection)
+ else:
+ status &= self._parseProjectContentLegacy(xSection)
+ else:
+ logger.warning("Ignored in xml", xSection.tag)
+
+ if not status:
+ self._state = XMLReadState.PARSING_ERROR
+ return False
+
+ if self._version == 0x0104:
+ self._state = XMLReadState.PARSED_OK
+ else:
+ self._state = XMLReadState.WAS_LEGACY
+
return True
##
# Internal Functions
##
+ def _parseProjectMeta(self, xSection):
+ """Parse the project section of the XML file.
+ """
+ logger.debug("Parsing xml ")
+ data = {}
+ authors = []
+ for xItem in xSection:
+ if xItem.tag == "name":
+ data["name"] = simplified(checkString(xItem.text, ""))
+ elif xItem.tag == "title":
+ data["title"] = simplified(checkString(xItem.text, ""))
+ elif xItem.tag == "author":
+ authors.append(simplified(checkString(xItem.text, "")))
+ elif xItem.tag == "saveCount":
+ data["saveCount"] = checkInt(xItem.text, 0)
+ elif xItem.tag == "autoCount":
+ data["autoCount"] = checkInt(xItem.text, 0)
+ elif xItem.tag == "editTime":
+ data["editTime"] = checkInt(xItem.text, 0)
+ else:
+ logger.warning("Ignored in xml", xItem.tag)
+
+ data["authors"] = authors
+ self._data["project"] = data
+
+ return True
+
+ def _parseProjectSettings(self, xSection):
+ """Parse the settings section of the XML file.
+ """
+ logger.debug("Parsing xml ")
+
+ data = {}
+ autoReplace = {}
+ titleFormat = {}
+ for xItem in xSection:
+ if xItem.tag == "doBackup":
+ data["doBackup"] = checkBool(xItem.text, False)
+ elif xItem.tag == "language":
+ data["language"] = checkStringNone(xItem.text, None)
+ elif xItem.tag == "spellCheck":
+ data["spellCheck"] = checkBool(xItem.text, False)
+ elif xItem.tag == "spellLang":
+ data["spellLang"] = checkStringNone(xItem.text, None)
+ elif xItem.tag == "lastEdited":
+ data["lastEdited"] = checkStringNone(xItem.text, None)
+ elif xItem.tag == "lastViewed":
+ data["lastViewed"] = checkStringNone(xItem.text, None)
+ elif xItem.tag == "lastNovel":
+ data["lastNovel"] = checkStringNone(xItem.text, None)
+ elif xItem.tag == "lastOutline":
+ data["lastOutline"] = checkStringNone(xItem.text, None)
+ elif xItem.tag == "lastWordCount":
+ data["lastWordCount"] = checkInt(xItem.text, 0)
+ elif xItem.tag == "novelWordCount":
+ data["novelWordCount"] = checkInt(xItem.text, 0)
+ elif xItem.tag == "notesWordCount":
+ data["notesWordCount"] = checkInt(xItem.text, 0)
+ elif xItem.tag == "status":
+ data["status"] = self._parseStatusImport(xItem, "status")
+ elif xItem.tag in ("import", "importance"):
+ data["import"] = self._parseStatusImport(xItem, "import")
+ elif xItem.tag == "autoReplace":
+ if self._version >= 0x0102:
+ for xEntry in xItem:
+ if xEntry.tag == "entry" and "key" in xEntry.attrib:
+ autoReplace[xEntry.attrib["key"]] = checkString(xEntry.text, "ERROR")
+ else: # Pre 1.2 format
+ for xEntry in xItem:
+ autoReplace[xEntry.tag] = checkString(xEntry.text, "ERROR")
+ elif xItem.tag == "titleFormat":
+ for xEntry in xItem:
+ titleFormat[xEntry.tag] = checkString(xEntry.text, "")
+ else:
+ logger.warning("Ignored in xml", xItem.tag)
+
+ data["autoReplace"] = autoReplace
+ data["titleFormat"] = titleFormat
+ self._data["settings"] = data
+
+ return True
+
+ def _parseProjectContent(self, xSection):
+ """Parse the content section of the XML file.
+ """
+ logger.debug("Parsing xml ")
+
+ data = []
+ for xItem in xSection:
+ if xItem.tag == "item":
+ item = {}
+ item["handle"] = xItem.attrib.get("handle", None)
+ item["parent"] = xItem.attrib.get("parent", None)
+ item["root"] = xItem.attrib.get("root", None)
+ item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
+ item["type"] = checkString(xItem.attrib.get("type", ""), "")
+ item["class"] = checkString(xItem.attrib.get("class", ""), "")
+ item["layout"] = checkString(xItem.attrib.get("layout", ""), "")
+ for xVal in xItem:
+ if xVal.tag == "meta":
+ item["expanded"] = checkBool(xVal.attrib.get("expanded", False), False)
+ item["heading"] = checkString(xVal.attrib.get("heading", "H0"), "H0")
+ item["charCount"] = checkInt(xVal.attrib.get("charCount", 0), 0)
+ item["wordCount"] = checkInt(xVal.attrib.get("wordCount", 0), 0)
+ item["paraCount"] = checkInt(xVal.attrib.get("paraCount", 0), 0)
+ item["cursorPos"] = checkInt(xVal.attrib.get("cursorPos", 0), 0)
+ elif xVal.tag == "name":
+ item["label"] = simplified(checkString(xVal.text, ""))
+ item["status"] = checkStringNone(xVal.attrib.get("status", None), None)
+ item["import"] = checkStringNone(xVal.attrib.get("import", None), None)
+ item["active"] = checkBool(xVal.attrib.get("active", False), False)
+
+ # ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
+ if "exported" in xVal.attrib:
+ item["active"] = checkBool(xVal.attrib.get("exported", False), False)
+ else:
+ logger.warning("Ignored in xml", xVal.tag)
+ data.append(item)
+ else:
+ logger.warning("Ignored item in xml", xItem.tag)
+
+ self._data["content"] = data
+
+ return True
+
+ def _parseProjectContentLegacy(self, xSection):
+ """Parse the content section of the XML file for version before 1.4.
+ """
+ logger.debug("Parsing xml (legacy format)")
+ depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE")
+ data = []
+ for xItem in xSection:
+ item = {}
+ if xItem.tag == "item":
+ item["handle"] = xItem.attrib.get("handle", None)
+ item["parent"] = xItem.attrib.get("parent", None)
+ item["root"] = None # Value was added in 1.4
+ item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
+ item["heading"] = "H0" # Value was added in 1.4
+
+ tmpStatus = ""
+ for xVal in xItem:
+ if xVal.tag == "name":
+ item["label"] = simplified(checkString(xVal.text, ""))
+ elif xVal.tag == "status":
+ tmpStatus = checkStringNone(xVal.text, None)
+ elif xVal.tag == "type":
+ item["type"] = checkString(xVal.text, "")
+ elif xVal.tag == "class":
+ item["class"] = checkString(xVal.text, "")
+ elif xVal.tag == "layout":
+ item["layout"] = checkString(xVal.text, "")
+ elif xVal.tag == "expanded":
+ item["expanded"] = checkBool(xVal.text, False)
+ elif xVal.tag == "exported": # Renamed to active in 1.4
+ item["active"] = checkBool(xVal.text, False)
+ elif xVal.tag == "charCount":
+ item["charCount"] = checkInt(xVal.text, 0)
+ elif xVal.tag == "wordCount":
+ item["wordCount"] = checkInt(xVal.text, 0)
+ elif xVal.tag == "paraCount":
+ item["paraCount"] = checkInt(xVal.text, 0)
+ elif xVal.tag == "cursorPos":
+ item["cursorPos"] = checkInt(xVal.text, 0)
+ else:
+ logger.warning("Ignored in xml", xVal.tag)
+
+ # Status was split into separate status/import with a key in 1.4
+ if item.get("class", "") in ("NOVEL", "ARCHIVE"):
+ item["status"] = self._getLegacyUnportStatus(tmpStatus, "status")
+ else:
+ item["import"] = self._getLegacyUnportStatus(tmpStatus, "import")
+
+ # A number of layouts were removed in 1.3
+ if item.get("layout", "") in depLayout:
+ item["layout"] = "DOCUMENT"
+
+ # The trast type was removed in 1.4
+ if item.get("type", "") == "TRASH":
+ item["type"] = "ROOT"
+
+ data.append(item)
+ else:
+ logger.warning("Ignored in xml", xItem.tag)
+
+ self._data["content"] = data
+
+ return True
+
+ def _parseStatusImport(self, xItem, type):
+ """Parse a status or importance entry.
+ """
+ data = self._statusData.get(type, {})
+ for xEntry in xItem:
+ if xEntry.tag == "entry":
+ key = xEntry.attrib.get("key", f"{type[0]}{len(data):06x}")
+ data[key] = {
+ "label": xEntry.text,
+ "count": checkInt(xEntry.attrib.get("count", 0), 0),
+ "colour": (
+ minmax(checkInt(xEntry.attrib.get("red", 0), 0), 0, 255),
+ minmax(checkInt(xEntry.attrib.get("green", 0), 0), 0, 255),
+ minmax(checkInt(xEntry.attrib.get("blue", 0), 0), 0, 255),
+ ),
+ }
+ self._statusData[type] = data
+
+ return data
+
+ def _getLegacyUnportStatus(self, label, type):
+ """Look up the label in defined status or importance values.
+ This is needed for file formats prior to 1.4 where the status
+ was saved as the label, not the key.
+ """
+ if not self._statusMap.get(type):
+ lookup = {}
+ for key, entry in self._statusData.get(type, {}).items():
+ lookup[entry.get("label", "")] = key
+ self._statusMap[type] = lookup
+ print(lookup)
+
+ return self._statusMap.get(type, {}).get(label, None)
+
# END Class ProjectXMLReader
diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py
index c466461e..1d19a08e 100644
--- a/novelwriter/core/status.py
+++ b/novelwriter/core/status.py
@@ -33,7 +33,7 @@ from lxml import etree
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
from PyQt5.QtCore import QRectF, Qt
-from novelwriter.common import checkInt, minmax, simplified
+from novelwriter.common import simplified
logger = logging.getLogger(__name__)
@@ -47,7 +47,6 @@ class NWStatus:
self._type = type
self._store = {}
- self._reverse = {}
self._default = None
self._iPX = novelwriter.CONFIG.pxInt(24)
@@ -90,7 +89,6 @@ class NWStatus:
"cols": col,
"count": count,
}
- self._reverse[name] = key
if self._default is None:
self._default = key
@@ -106,7 +104,6 @@ class NWStatus:
if self._store[key]["count"] > 0:
return False
- del self._reverse[self._store[key]["name"]]
del self._store[key]
keys = list(self._store.keys())
@@ -123,8 +120,6 @@ class NWStatus:
"""
if self._isKey(value) and value in self._store:
return value
- elif value in self._reverse:
- return self._reverse[value]
elif self._default is not None:
return self._default
else:
@@ -222,21 +217,17 @@ class NWStatus:
return True
- def unpackXML(self, xParent):
- """Unpack an XML tree and set the class values.
+ def unpack(self, data):
+ """Unpack a data dictionary and set the class values.
"""
self._store = {}
- self._reverse = {}
self._default = None
- for xChild in xParent:
- key = xChild.attrib.get("key", None)
- name = xChild.text.strip()
- count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
- red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
- green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
- blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
- self.write(key, name, (red, green, blue), count)
+ for key, entry in data.items():
+ label = entry.get("label", "")
+ colour = entry.get("colour", (100, 100, 100))
+ count = entry.get("count", 0)
+ self.write(key, label, colour, count)
return True
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 506556cd..1db11a27 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -126,18 +126,14 @@ class NWTree:
tItem.packXML(xContent)
return
- def unpackXML(self, xContent):
- """Iterate through all items of a content XML object and add
- them to the project tree.
+ def unpack(self, data):
+ """Iterate through all items of a list and add them to the
+ project tree.
"""
- if xContent.tag != "content":
- logger.error("XML entry is not a NWTree")
- return False
-
self.clear()
- for xItem in xContent:
+ for item in data:
nwItem = NWItem(self.theProject)
- if nwItem.unpackXML(xItem):
+ if nwItem.unpack(item):
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount()
From e887ec6991113c27c7e5c350eee7f62e11eab416 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 30 Oct 2022 23:18:18 +0100
Subject: [PATCH 03/18] Move project name to project data class
---
novelwriter/core/index.py | 2 +-
novelwriter/core/project.py | 64 +++++------
novelwriter/core/projectdata.py | 112 ++++++++++++++++++++
novelwriter/core/projectxml.py | 27 ++---
novelwriter/core/tohtml.py | 2 +-
novelwriter/dialogs/projdetails.py | 2 +-
novelwriter/dialogs/projsettings.py | 4 +-
novelwriter/guimain.py | 6 +-
novelwriter/tools/build.py | 4 +-
tests/test_core/test_core_project.py | 16 +--
tests/test_dialogs/test_dlg_projsettings.py | 2 +-
tests/test_gui/test_gui_guimain.py | 4 +-
tests/tools.py | 2 +-
13 files changed, 174 insertions(+), 73 deletions(-)
create mode 100644 novelwriter/core/projectdata.py
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 1058628b..ce1a1278 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -75,7 +75,7 @@ class NWIndex:
return
def __repr__(self):
- return f""
+ return f""
##
# Properties
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 76294a08..50fe0cef 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -38,9 +38,10 @@ from PyQt5.QtCore import QCoreApplication
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException
from novelwriter.common import (
- checkString, checkStringNone, isHandle, formatTimeStamp,
- makeFileNameSafe, hexToInt, minmax, simplified
+ checkString, checkStringNone, isHandle, formatTimeStamp, makeFileNameSafe,
+ hexToInt, minmax, simplified
)
+from novelwriter.constants import trConst, nwFiles, nwLabels
from novelwriter.core.tree import NWTree
from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
@@ -48,7 +49,7 @@ from novelwriter.core.status import NWStatus
from novelwriter.core.options import OptionState
from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState
-from novelwriter.constants import trConst, nwFiles, nwLabels
+from novelwriter.core.projectdata import NWProjectData
logger = logging.getLogger(__name__)
@@ -63,7 +64,8 @@ class NWProject:
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
- self._data = {}
+ self._data = NWProjectData()
+ self._raw = {}
# Core Elements
self._optState = OptionState(self) # Project-specific GUI options
@@ -91,7 +93,6 @@ class NWProject:
self.projFiles = [] # A list of all files in the content folder on load
# Project Meta
- self.projName = "" # Project name
self.bookTitle = "" # The final title; should only be used for exports
self.bookAuthors = [] # A list of book authors
@@ -125,6 +126,10 @@ class NWProject:
# Properties
##
+ @property
+ def data(self):
+ return self._data
+
@property
def index(self):
return self._projIndex
@@ -263,7 +268,6 @@ class NWProject:
self.projSpell = None
self.projLang = None
self.projFiles = []
- self.projName = ""
self.bookTitle = ""
self.bookAuthors = []
self.autoReplace = {}
@@ -328,14 +332,14 @@ class NWProject:
if not self.setProjectPath(projPath, newProject=True):
return False
- self.setProjectName(projName)
+ self.data.setName(projName)
self.setBookTitle(projTitle)
self.setBookAuthors(projAuthors)
hNovelRoot = self.newRoot(nwItemClass.NOVEL)
hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot)
- titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
+ titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self._data.name)
if self.bookAuthors:
titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors())
@@ -480,8 +484,9 @@ class NWProject:
# Open The Project XML File
# =========================
+ self._data = NWProjectData()
xmlReader = ProjectXMLReader(fileName)
- xmlParsed = xmlReader.read()
+ xmlParsed = xmlReader.read(self._data)
xmlData = xmlReader.data
print(json.dumps(xmlData, indent=2))
@@ -510,7 +515,7 @@ class NWProject:
self.clearProject()
return False
- self._data = xmlData
+ self._raw = xmlData
logger.debug("XML root is '%s'", nwxRoot)
logger.debug("File version is '%s'", xmlVersion)
@@ -552,16 +557,13 @@ class NWProject:
# Extract Data
# ============
- xmlProject = xmlData.get("project", {})
+ self.bookTitle = self._data.title
+ self.bookAuthors = self._data.autors
+ self.saveCount = self._data.saveCount
+ self.autoCount = self._data.autoCount
+ self.editTime = self._data.editTime
- self.projName = xmlProject.get("name", "")
- self.bookTitle = xmlProject.get("title", "")
- self.bookAuthors = xmlProject.get("authors", [])
- self.saveCount = xmlProject.get("saveCount", 0)
- self.autoCount = xmlProject.get("autoCount", 0)
- self.editTime = xmlProject.get("editTime", 0)
-
- logger.info("Project Name: '%s'", self.projName)
+ logger.info("Project Name: '%s'", self._data.name)
logger.info("Project Title: '%s'", self.bookTitle)
xmlSettings = xmlData.get("settings", {})
@@ -601,7 +603,7 @@ class NWProject:
self._deprecatedFiles()
# Update recent projects
- self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
+ self.mainConf.updateRecentCache(self.projPath, self._data.name, self.lastWCount, time())
self.mainConf.saveRecentCache()
# Check the project tree consistency
@@ -621,7 +623,7 @@ class NWProject:
self._writeLockFile()
self.setProjectChanged(False)
- self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self.projName))
+ self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name))
return True
@@ -662,7 +664,7 @@ class NWProject:
# Save Project Meta
xProject = etree.SubElement(nwXML, "project")
- self._packProjectValue(xProject, "name", self.projName)
+ self._packProjectValue(xProject, "name", self._data.name)
self._packProjectValue(xProject, "title", self.bookTitle)
self._packProjectValue(xProject, "author", self.bookAuthors)
self._packProjectValue(xProject, "saveCount", str(self.saveCount))
@@ -734,11 +736,11 @@ class NWProject:
self._optState.saveSettings()
# Update recent projects
- self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
+ self.mainConf.updateRecentCache(self.projPath, self._data.name, self.currWCount, saveTime)
self.mainConf.saveRecentCache()
self._writeLockFile()
- self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self.projName))
+ self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name))
self.setProjectChanged(False)
return True
@@ -800,14 +802,14 @@ class NWProject:
), nwAlert.ERROR)
return False
- if not self.projName:
+ if not self._data.name:
self.mainGui.makeAlert(self.tr(
"Cannot backup project because no project name is set. "
"Please set a Working Title in Project Settings."
), nwAlert.ERROR)
return False
- cleanName = makeFileNameSafe(self.projName)
+ cleanName = makeFileNameSafe(self._data.name)
baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName))
if not os.path.isdir(baseDir):
try:
@@ -953,14 +955,6 @@ class NWProject:
return True
- def setProjectName(self, projName):
- """Set the project name, This is the the name used for backup
- files etc.
- """
- self.projName = simplified(projName)
- self.setProjectChanged(True)
- return True
-
def setBookTitle(self, bookTitle):
"""Set the book title, that is, the title to include in exports.
"""
@@ -998,7 +992,7 @@ class NWProject:
), nwAlert.WARN)
return False
- if self.projName == "":
+ if self._data.name == "":
self.mainGui.makeAlert(self.tr(
"You must set a valid project name in Project Settings to "
"use the automatic project backup feature."
diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py
new file mode 100644
index 00000000..f5719949
--- /dev/null
+++ b/novelwriter/core/projectdata.py
@@ -0,0 +1,112 @@
+"""
+novelWriter – Project Data Class
+================================
+Class for holding the project settings
+
+File History:
+Created: 2022-10-30 [2.0rc1]
+
+This file is a part of novelWriter
+Copyright 2018–2022, Veronica Berglyd Olsen
+
+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 .
+"""
+
+import logging
+
+from novelwriter.common import checkInt, simplified
+
+logger = logging.getLogger(__name__)
+
+
+class NWProjectData:
+
+ def __init__(self):
+
+ # Project Meta
+ self._name = ""
+ self._title = ""
+ self._authors = []
+ self._saveCount = 0
+ self._autoCount = 0
+ self._editTime = 0
+
+ # Internal
+ self._changed = False
+
+ return
+
+ ##
+ # Properties
+ ##
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def title(self):
+ return self._title
+
+ @property
+ def autors(self):
+ return self._authors
+
+ @property
+ def saveCount(self):
+ return self._saveCount
+
+ @property
+ def autoCount(self):
+ return self._autoCount
+
+ @property
+ def editTime(self):
+ return self._editTime
+
+ ##
+ # Setters
+ ##
+
+ def setName(self, value):
+ self._name = simplified(str(value))
+ self._changed = True
+ return
+
+ def setTitle(self, value):
+ self._title = simplified(str(value))
+ self._changed = True
+ return
+
+ def addAuthor(self, value):
+ self._authors.append(simplified(str(value)))
+ self._changed = True
+ return
+
+ def setSaveCount(self, value):
+ self._saveCount = checkInt(value, 0)
+ self._changed = True
+ return
+
+ def setAutoCount(self, value):
+ self._autoCount = checkInt(value, 0)
+ self._changed = True
+ return
+
+ def setEditTime(self, value):
+ self._editTime = checkInt(value, 0)
+ self._changed = True
+ return
+
+# END Class NWProjectData
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 2aa2d248..7b78dd2c 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -4,7 +4,8 @@ novelWriter – Project XML Read/Write
Classes for reading and writing the project XML file
File History:
-Created: 2022-09-28 [1.7.b1]
+Created: 2022-09-28 [2.0rc1] ProjectXMLReader
+Created: 2022-09-28 [2.0rc1] XMLReadState
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -90,7 +91,7 @@ class ProjectXMLReader:
# Methods
##
- def read(self):
+ def read(self, projData):
"""Read and parse the project XML file.
"""
self._data = {}
@@ -154,7 +155,7 @@ class ProjectXMLReader:
status = True
for xSection in xRoot:
if xSection.tag == "project":
- status &= self._parseProjectMeta(xSection)
+ status &= self._parseProjectMeta(xSection, projData)
elif xSection.tag == "settings":
status &= self._parseProjectSettings(xSection)
elif xSection.tag == "content":
@@ -180,31 +181,25 @@ class ProjectXMLReader:
# Internal Functions
##
- def _parseProjectMeta(self, xSection):
+ def _parseProjectMeta(self, xSection, projData):
"""Parse the project section of the XML file.
"""
logger.debug("Parsing xml ")
- data = {}
- authors = []
for xItem in xSection:
if xItem.tag == "name":
- data["name"] = simplified(checkString(xItem.text, ""))
+ projData.setName(xItem.text)
elif xItem.tag == "title":
- data["title"] = simplified(checkString(xItem.text, ""))
+ projData.setTitle(xItem.text)
elif xItem.tag == "author":
- authors.append(simplified(checkString(xItem.text, "")))
+ projData.addAuthor(xItem.text)
elif xItem.tag == "saveCount":
- data["saveCount"] = checkInt(xItem.text, 0)
+ projData.setSaveCount(xItem.text)
elif xItem.tag == "autoCount":
- data["autoCount"] = checkInt(xItem.text, 0)
+ projData.setAutoCount(xItem.text)
elif xItem.tag == "editTime":
- data["editTime"] = checkInt(xItem.text, 0)
+ projData.setEditTime(xItem.text)
else:
logger.warning("Ignored in xml", xItem.tag)
-
- data["authors"] = authors
- self._data["project"] = data
-
return True
def _parseProjectSettings(self, xSection):
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 2110300c..88647d63 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -315,7 +315,7 @@ class ToHtml(Tokenizer):
"