Complete the XML parsing
This commit is contained in:
+38
-69
@@ -148,7 +148,7 @@ class NWItem:
|
|||||||
return self._cursorPos
|
return self._cursorPos
|
||||||
|
|
||||||
##
|
##
|
||||||
# XML Pack/Unpack
|
# Pack/Unpack Data
|
||||||
##
|
##
|
||||||
|
|
||||||
def packXML(self, xParent):
|
def packXML(self, xParent):
|
||||||
@@ -167,7 +167,7 @@ class NWItem:
|
|||||||
metaAttrib = {}
|
metaAttrib = {}
|
||||||
metaAttrib["expanded"] = str(self._expanded)
|
metaAttrib["expanded"] = str(self._expanded)
|
||||||
if self._type == nwItemType.FILE:
|
if self._type == nwItemType.FILE:
|
||||||
metaAttrib["mainHeading"] = str(self._heading)
|
metaAttrib["heading"] = str(self._heading)
|
||||||
metaAttrib["charCount"] = str(self._charCount)
|
metaAttrib["charCount"] = str(self._charCount)
|
||||||
metaAttrib["wordCount"] = str(self._wordCount)
|
metaAttrib["wordCount"] = str(self._wordCount)
|
||||||
metaAttrib["paraCount"] = str(self._paraCount)
|
metaAttrib["paraCount"] = str(self._paraCount)
|
||||||
@@ -185,70 +185,31 @@ class NWItem:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def unpackXML(self, xItem):
|
def unpack(self, data):
|
||||||
"""Set the values from an XML entry of type 'item'.
|
"""Set the values from a data dictionary.
|
||||||
"""
|
"""
|
||||||
if xItem.tag != "item":
|
if "handle" in data:
|
||||||
logger.error("XML entry is not an NWItem")
|
self.setHandle(data["handle"])
|
||||||
return False
|
|
||||||
|
|
||||||
if "handle" in xItem.attrib:
|
|
||||||
self.setHandle(xItem.attrib["handle"])
|
|
||||||
else:
|
else:
|
||||||
logger.error("XML item entry does not have a handle")
|
logger.error("XML item entry does not have a handle")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.setParent(xItem.attrib.get("parent", None))
|
self.setParent(data.get("parent", None))
|
||||||
self.setRoot(xItem.attrib.get("root", None))
|
self.setRoot(data.get("root", None))
|
||||||
self.setOrder(xItem.attrib.get("order", 0))
|
self.setOrder(data.get("order", 0))
|
||||||
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
|
self.setType(data.get("type", nwItemType.NO_TYPE))
|
||||||
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
|
self.setClass(data.get("class", nwItemClass.NO_CLASS))
|
||||||
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
|
self.setLayout(data.get("layout", nwItemLayout.NO_LAYOUT))
|
||||||
|
self.setExpanded(data.get("expanded", False))
|
||||||
for xValue in xItem:
|
self.setMainHeading(data.get("mainHeading", "H0"))
|
||||||
if xValue.tag == "meta":
|
self.setCharCount(data.get("charCount", 0))
|
||||||
self.setExpanded(xValue.attrib.get("expanded", False))
|
self.setWordCount(data.get("wordCount", 0))
|
||||||
self.setMainHeading(xValue.attrib.get("mainHeading", "H0"))
|
self.setParaCount(data.get("paraCount", 0))
|
||||||
self.setCharCount(xValue.attrib.get("charCount", 0))
|
self.setCursorPos(data.get("cursorPos", 0))
|
||||||
self.setWordCount(xValue.attrib.get("wordCount", 0))
|
self.setName(data.get("label", ""))
|
||||||
self.setParaCount(xValue.attrib.get("paraCount", 0))
|
self.setStatus(data.get("status", None))
|
||||||
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
|
self.setImport(data.get("import", None))
|
||||||
elif xValue.tag == "name":
|
self.setActive(data.get("active", True))
|
||||||
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)
|
|
||||||
|
|
||||||
# Make some checks to ensure consistency
|
# Make some checks to ensure consistency
|
||||||
if self._type == nwItemType.ROOT:
|
if self._type == nwItemType.ROOT:
|
||||||
@@ -451,8 +412,6 @@ class NWItem:
|
|||||||
self._type = value
|
self._type = value
|
||||||
elif isItemType(value):
|
elif isItemType(value):
|
||||||
self._type = nwItemType[value]
|
self._type = nwItemType[value]
|
||||||
elif value == "TRASH":
|
|
||||||
self._type = nwItemType.ROOT
|
|
||||||
else:
|
else:
|
||||||
logger.error("Unrecognised item type '%s'", value)
|
logger.error("Unrecognised item type '%s'", value)
|
||||||
self._type = nwItemType.NO_TYPE
|
self._type = nwItemType.NO_TYPE
|
||||||
@@ -479,8 +438,6 @@ class NWItem:
|
|||||||
self._layout = value
|
self._layout = value
|
||||||
elif isItemLayout(value):
|
elif isItemLayout(value):
|
||||||
self._layout = nwItemLayout[value]
|
self._layout = nwItemLayout[value]
|
||||||
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
|
|
||||||
self._layout = nwItemLayout.DOCUMENT
|
|
||||||
else:
|
else:
|
||||||
logger.error("Unrecognised item layout '%s'", value)
|
logger.error("Unrecognised item layout '%s'", value)
|
||||||
self._layout = nwItemLayout.NO_LAYOUT
|
self._layout = nwItemLayout.NO_LAYOUT
|
||||||
@@ -532,25 +489,37 @@ class NWItem:
|
|||||||
def setCharCount(self, count):
|
def setCharCount(self, count):
|
||||||
"""Set the character count, and ensure that it is an integer.
|
"""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
|
return
|
||||||
|
|
||||||
def setWordCount(self, count):
|
def setWordCount(self, count):
|
||||||
"""Set the word count, and ensure that it is an integer.
|
"""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
|
return
|
||||||
|
|
||||||
def setParaCount(self, count):
|
def setParaCount(self, count):
|
||||||
"""Set the paragraph count, and ensure that it is an integer.
|
"""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
|
return
|
||||||
|
|
||||||
def setCursorPos(self, position):
|
def setCursorPos(self, position):
|
||||||
"""Set the cursor position, and ensure that it is an integer.
|
"""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
|
return
|
||||||
|
|
||||||
def saveInitialCount(self):
|
def saveInitialCount(self):
|
||||||
|
|||||||
+70
-141
@@ -35,18 +35,19 @@ from functools import partial
|
|||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication
|
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.tree import NWTree
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.index import NWIndex
|
from novelwriter.core.index import NWIndex
|
||||||
from novelwriter.core.status import NWStatus
|
from novelwriter.core.status import NWStatus
|
||||||
from novelwriter.core.options import OptionState
|
from novelwriter.core.options import OptionState
|
||||||
from novelwriter.core.document import NWDoc
|
from novelwriter.core.document import NWDoc
|
||||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState
|
||||||
from novelwriter.error import logException
|
|
||||||
from novelwriter.common import (
|
|
||||||
checkString, checkBool, checkInt, checkStringNone, isHandle, formatTimeStamp,
|
|
||||||
makeFileNameSafe, hexToInt, minmax, simplified
|
|
||||||
)
|
|
||||||
from novelwriter.constants import trConst, nwFiles, nwLabels
|
from novelwriter.constants import trConst, nwFiles, nwLabels
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -62,6 +63,8 @@ class NWProject:
|
|||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
|
|
||||||
|
self._data = {}
|
||||||
|
|
||||||
# Core Elements
|
# Core Elements
|
||||||
self._optState = OptionState(self) # Project-specific GUI options
|
self._optState = OptionState(self) # Project-specific GUI options
|
||||||
self._projTree = NWTree(self) # The project tree
|
self._projTree = NWTree(self) # The project tree
|
||||||
@@ -477,80 +480,45 @@ class NWProject:
|
|||||||
# Open The Project XML File
|
# Open The Project XML File
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
try:
|
xmlReader = ProjectXMLReader(fileName)
|
||||||
nwXML = etree.parse(fileName)
|
xmlParsed = xmlReader.read()
|
||||||
except Exception as exc:
|
xmlData = xmlReader.data
|
||||||
self.mainGui.makeAlert(self.tr(
|
|
||||||
"Failed to parse project xml."
|
|
||||||
), nwAlert.ERROR, exception=exc)
|
|
||||||
|
|
||||||
# Trying to open backup file instead
|
print(json.dumps(xmlData, indent=2))
|
||||||
backFile = fileName[:-3]+"bak"
|
|
||||||
if os.path.isfile(backFile):
|
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(
|
self.mainGui.makeAlert(self.tr(
|
||||||
"Attempting to open backup project file instead."
|
"Project file does not appear to be a novelWriterXML file."
|
||||||
), nwAlert.INFO)
|
), nwAlert.ERROR)
|
||||||
try:
|
elif xmlReader.state == XMLReadState.UNKNOWN_VERSION:
|
||||||
nwXML = etree.parse(backFile)
|
self.mainGui.makeAlert(self.tr(
|
||||||
except Exception as exc:
|
"Unknown or unsupported novelWriter project file format. "
|
||||||
self.mainGui.makeAlert(self.tr(
|
"The project cannot be opened by this version of novelWriter. "
|
||||||
"Failed to parse project xml."
|
"The file was saved with novelWriter version {0}."
|
||||||
), nwAlert.ERROR, exception=exc)
|
).format(appVersion), nwAlert.ERROR)
|
||||||
self.clearProject()
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
self.clearProject()
|
self.mainGui.makeAlert(self.tr(
|
||||||
return False
|
"Failed to parse project xml."
|
||||||
|
), nwAlert.ERROR)
|
||||||
|
|
||||||
xRoot = nwXML.getroot()
|
self.clearProject()
|
||||||
nwxRoot = xRoot.tag
|
return False
|
||||||
|
|
||||||
appVersion = xRoot.attrib.get("appVersion", self.tr("Unknown"))
|
self._data = xmlData
|
||||||
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
|
|
||||||
fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown"))
|
|
||||||
|
|
||||||
logger.debug("XML root is '%s'", nwxRoot)
|
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":
|
if xmlReader.state == XMLReadState.WAS_LEGACY:
|
||||||
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:
|
|
||||||
msgYes = self.mainGui.askQuestion(
|
msgYes = self.mainGui.askQuestion(
|
||||||
self.tr("File Version"),
|
self.tr("File Version"),
|
||||||
self.tr(
|
self.tr(
|
||||||
@@ -581,79 +549,40 @@ class NWProject:
|
|||||||
self.clearProject()
|
self.clearProject()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Start Parsing the XML
|
# Extract Data
|
||||||
# =====================
|
# ============
|
||||||
|
|
||||||
for xChild in xRoot:
|
xmlProject = xmlData.get("project", {})
|
||||||
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)
|
|
||||||
|
|
||||||
elif xChild.tag == "settings":
|
self.projName = xmlProject.get("name", "")
|
||||||
logger.debug("Found project settings")
|
self.bookTitle = xmlProject.get("title", "")
|
||||||
for xItem in xChild:
|
self.bookAuthors = xmlProject.get("authors", [])
|
||||||
if xItem.text is None:
|
self.saveCount = xmlProject.get("saveCount", 0)
|
||||||
continue
|
self.autoCount = xmlProject.get("autoCount", 0)
|
||||||
if xItem.tag == "doBackup":
|
self.editTime = xmlProject.get("editTime", 0)
|
||||||
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)
|
|
||||||
|
|
||||||
elif xChild.tag == "content":
|
logger.info("Project Name: '%s'", self.projName)
|
||||||
logger.debug("Found project content")
|
logger.info("Project Title: '%s'", self.bookTitle)
|
||||||
self._projTree.unpackXML(xChild)
|
|
||||||
|
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()
|
self._optState.loadSettings()
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,33 @@ import logging
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
from novelwriter.common import (
|
||||||
|
checkBool, checkInt, checkStringNone, minmax, simplified, checkString
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
NUM_VERSION = {
|
||||||
|
"1.0": 0x0100,
|
||||||
|
"1.1": 0x0101,
|
||||||
|
"1.2": 0x0102,
|
||||||
|
"1.3": 0x0103,
|
||||||
|
"1.4": 0x0104,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class XMLReadState(Enum):
|
class XMLReadState(Enum):
|
||||||
|
|
||||||
NO_ACTION = 0
|
NO_ACTION = 0
|
||||||
NO_ERROR = 1
|
NO_ERROR = 1
|
||||||
PARSED_BACKUP = 2
|
PARSED_BACKUP = 2
|
||||||
CANNOT_PARSE = 3
|
CANNOT_PARSE = 3
|
||||||
|
NOT_NWX_FILE = 4
|
||||||
|
UNKNOWN_VERSION = 5
|
||||||
|
PARSING_ERROR = 6
|
||||||
|
PARSED_OK = 7
|
||||||
|
WAS_LEGACY = 8
|
||||||
|
|
||||||
# END Class XMLReadState
|
# END Class XMLReadState
|
||||||
|
|
||||||
@@ -49,11 +67,34 @@ class ProjectXMLReader:
|
|||||||
self._path = path
|
self._path = path
|
||||||
self._state = XMLReadState.NO_ACTION
|
self._state = XMLReadState.NO_ACTION
|
||||||
|
|
||||||
|
self._data = {}
|
||||||
|
self._version = 0x0000
|
||||||
|
self._statusData = {}
|
||||||
|
self._statusMap = {}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
##
|
||||||
|
# Methods
|
||||||
|
##
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
|
"""Read and parse the project XML file.
|
||||||
"""
|
"""
|
||||||
"""
|
self._data = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml = etree.parse(self._path)
|
xml = etree.parse(self._path)
|
||||||
self._state = XMLReadState.NO_ERROR
|
self._state = XMLReadState.NO_ERROR
|
||||||
@@ -76,12 +117,292 @@ class ProjectXMLReader:
|
|||||||
else:
|
else:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# 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
|
# END Class ProjectXMLReader
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from lxml import etree
|
|||||||
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
||||||
from PyQt5.QtCore import QRectF, Qt
|
from PyQt5.QtCore import QRectF, Qt
|
||||||
|
|
||||||
from novelwriter.common import checkInt, minmax, simplified
|
from novelwriter.common import simplified
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ class NWStatus:
|
|||||||
|
|
||||||
self._type = type
|
self._type = type
|
||||||
self._store = {}
|
self._store = {}
|
||||||
self._reverse = {}
|
|
||||||
self._default = None
|
self._default = None
|
||||||
|
|
||||||
self._iPX = novelwriter.CONFIG.pxInt(24)
|
self._iPX = novelwriter.CONFIG.pxInt(24)
|
||||||
@@ -90,7 +89,6 @@ class NWStatus:
|
|||||||
"cols": col,
|
"cols": col,
|
||||||
"count": count,
|
"count": count,
|
||||||
}
|
}
|
||||||
self._reverse[name] = key
|
|
||||||
|
|
||||||
if self._default is None:
|
if self._default is None:
|
||||||
self._default = key
|
self._default = key
|
||||||
@@ -106,7 +104,6 @@ class NWStatus:
|
|||||||
if self._store[key]["count"] > 0:
|
if self._store[key]["count"] > 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
del self._reverse[self._store[key]["name"]]
|
|
||||||
del self._store[key]
|
del self._store[key]
|
||||||
|
|
||||||
keys = list(self._store.keys())
|
keys = list(self._store.keys())
|
||||||
@@ -123,8 +120,6 @@ class NWStatus:
|
|||||||
"""
|
"""
|
||||||
if self._isKey(value) and value in self._store:
|
if self._isKey(value) and value in self._store:
|
||||||
return value
|
return value
|
||||||
elif value in self._reverse:
|
|
||||||
return self._reverse[value]
|
|
||||||
elif self._default is not None:
|
elif self._default is not None:
|
||||||
return self._default
|
return self._default
|
||||||
else:
|
else:
|
||||||
@@ -222,21 +217,17 @@ class NWStatus:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unpackXML(self, xParent):
|
def unpack(self, data):
|
||||||
"""Unpack an XML tree and set the class values.
|
"""Unpack a data dictionary and set the class values.
|
||||||
"""
|
"""
|
||||||
self._store = {}
|
self._store = {}
|
||||||
self._reverse = {}
|
|
||||||
self._default = None
|
self._default = None
|
||||||
|
|
||||||
for xChild in xParent:
|
for key, entry in data.items():
|
||||||
key = xChild.attrib.get("key", None)
|
label = entry.get("label", "")
|
||||||
name = xChild.text.strip()
|
colour = entry.get("colour", (100, 100, 100))
|
||||||
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
|
count = entry.get("count", 0)
|
||||||
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
|
self.write(key, label, colour, count)
|
||||||
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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -126,18 +126,14 @@ class NWTree:
|
|||||||
tItem.packXML(xContent)
|
tItem.packXML(xContent)
|
||||||
return
|
return
|
||||||
|
|
||||||
def unpackXML(self, xContent):
|
def unpack(self, data):
|
||||||
"""Iterate through all items of a content XML object and add
|
"""Iterate through all items of a list and add them to the
|
||||||
them to the project tree.
|
project tree.
|
||||||
"""
|
"""
|
||||||
if xContent.tag != "content":
|
|
||||||
logger.error("XML entry is not a NWTree")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
for xItem in xContent:
|
for item in data:
|
||||||
nwItem = NWItem(self.theProject)
|
nwItem = NWItem(self.theProject)
|
||||||
if nwItem.unpackXML(xItem):
|
if nwItem.unpack(item):
|
||||||
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
||||||
nwItem.saveInitialCount()
|
nwItem.saveInitialCount()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user