Complete the XML parsing

This commit is contained in:
Veronica Berglyd Olsen
2022-10-30 22:51:31 +01:00
parent 6d1734b655
commit ea80fd3c71
5 changed files with 447 additions and 241 deletions
+38 -69
View File
@@ -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):
+70 -141
View File
@@ -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()
+326 -5
View File
@@ -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 <root/%s> 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 <root/project>")
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 <root/project/%s> 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 <root/settings>")
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 <root/settings/%s> 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 <root/content>")
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 <root/content/item/%s> in xml", xVal.tag)
data.append(item)
else:
logger.warning("Ignored item <root/content/%s> 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 <root/content> (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 <root/content/item/%s> 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 <root/content/%s> 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
+8 -17
View File
@@ -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
+5 -9
View File
@@ -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()