Complete the XML parsing
This commit is contained in:
+38
-69
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user