Complete the xml writer class

This commit is contained in:
Veronica Berglyd Olsen
2022-10-31 20:05:20 +01:00
parent 3a161e90f9
commit a760aff826
8 changed files with 220 additions and 222 deletions
+31 -45
View File
@@ -25,8 +25,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from lxml import etree
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
@@ -151,39 +149,40 @@ class NWItem:
# Pack/Unpack Data
##
def packXML(self, xParent):
"""Pack all the data in the class instance into an XML object.
def pack(self):
"""Pack all the data in the class instance into a dictionary.
"""
itemAttrib = {}
itemAttrib["handle"] = str(self._handle)
itemAttrib["parent"] = str(self._parent)
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order)
itemAttrib["type"] = str(self._type.name)
itemAttrib["class"] = str(self._class.name)
item = {}
meta = {}
name = {}
item["handle"] = str(self._handle)
item["parent"] = str(self._parent)
item["root"] = str(self._root)
item["order"] = str(self._order)
item["type"] = str(self._type.name)
item["class"] = str(self._class.name)
meta["expanded"] = str(self._expanded)
name["status"] = str(self._status)
name["import"] = str(self._import)
if self._type == nwItemType.FILE:
itemAttrib["layout"] = str(self._layout.name)
item["layout"] = str(self._layout.name)
meta["heading"] = str(self._heading)
meta["charCount"] = str(self._charCount)
meta["wordCount"] = str(self._wordCount)
meta["paraCount"] = str(self._paraCount)
meta["cursorPos"] = str(self._cursorPos)
name["active"] = str(self._active)
metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE:
metaAttrib["heading"] = str(self._heading)
metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount)
metaAttrib["cursorPos"] = str(self._cursorPos)
data = {
"name": str(self._name),
"itemAttr": item,
"metaAttr": meta,
"nameAttr": name,
}
nameAttrib = {}
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["active"] = str(self._active)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
self._subPack(xPack, "meta", attrib=metaAttrib)
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
return
return data
def unpack(self, data):
"""Set the values from a data dictionary.
@@ -201,7 +200,7 @@ class NWItem:
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.setMainHeading(data.get("heading", "H0"))
self.setCharCount(data.get("charCount", 0))
self.setWordCount(data.get("wordCount", 0))
self.setParaCount(data.get("paraCount", 0))
@@ -224,19 +223,6 @@ class NWItem:
return True
@staticmethod
def _subPack(xParent, name, attrib=None, text=None, none=True):
"""Pack the values into an XML element.
"""
if not none and (text is None or text == "None"):
return None
xAttr = {} if attrib is None else attrib
xSub = etree.SubElement(xParent, name, attrib=xAttr)
if text is not None:
xSub.text = text
return
##
# Lookup Methods
##
+8 -101
View File
@@ -30,7 +30,6 @@ import logging
import novelwriter
from time import time
from lxml import etree
from functools import partial
from PyQt5.QtCore import QCoreApplication
@@ -47,7 +46,7 @@ from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
from novelwriter.core.options import OptionState
from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.core.projectdata import NWProjectData
logger = logging.getLogger(__name__)
@@ -585,86 +584,16 @@ class NWProject:
else:
self._data.incSaveCount()
# Root element and project details
logger.debug("Writing project meta")
nwXML = etree.Element("novelWriterXML", attrib={
"appVersion": str(novelwriter.__version__),
"hexVersion": str(novelwriter.__hexversion__),
"fileVersion": self.FILE_VERSION,
"timeStamp": formatTimeStamp(saveTime),
})
self.updateWordCounts()
self.countStatus()
saveTime = time()
editTime = int(self._data.editTime + saveTime - self._projOpened)
# Save Project Meta
xProject = etree.SubElement(nwXML, "project")
self._packProjectValue(xProject, "name", self._data.name)
self._packProjectValue(xProject, "title", self._data.title)
self._packProjectValue(xProject, "author", self._data.authors)
self._packProjectValue(xProject, "saveCount", str(self._data.saveCount))
self._packProjectValue(xProject, "autoCount", str(self._data.autoCount))
self._packProjectValue(xProject, "editTime", str(editTime))
# Save Project Settings
xSettings = etree.SubElement(nwXML, "settings")
self._packProjectValue(xSettings, "doBackup", self._data.doBackup)
self._packProjectValue(xSettings, "language", self._data.language)
self._packProjectValue(xSettings, "spellCheck", self._data.spellCheck)
self._packProjectValue(xSettings, "spellLang", self._data.spellLang)
self._packProjectValue(xSettings, "lastEdited", self._data.getLastHandle("editor"))
self._packProjectValue(xSettings, "lastViewed", self._data.getLastHandle("viewer"))
self._packProjectValue(xSettings, "lastNovel", self._data.getLastHandle("noveltree"))
self._packProjectValue(xSettings, "lastOutline", self._data.getLastHandle("outline"))
self._packProjectValue(xSettings, "lastWordCount", self._data.getCurrCount("total"))
self._packProjectValue(xSettings, "novelWordCount", self._data.getCurrCount("novel"))
self._packProjectValue(xSettings, "notesWordCount", self._data.getCurrCount("notes"))
self._packProjectKeyValue(xSettings, "autoReplace", self._data.autoReplace)
xTitleFmt = etree.SubElement(xSettings, "titleFormat")
for aKey, aValue in self._data.titleFormat.items():
if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue)
# Save Status/Importance
self.countStatus()
xStatus = etree.SubElement(xSettings, "status")
self._data.itemStatus.packXML(xStatus)
xStatus = etree.SubElement(xSettings, "importance")
self._data.itemImport.packXML(xStatus)
# Save Tree Content
logger.debug("Writing project content")
self._projTree.packXML(nwXML)
# Write the xml tree to file
tempFile = os.path.join(self.projPath, nwFiles.PROJ_FILE+"~")
saveFile = os.path.join(self.projPath, nwFiles.PROJ_FILE)
backFile = os.path.join(self.projPath, nwFiles.PROJ_FILE[:-3]+"bak")
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
nwXML,
pretty_print=True,
encoding="utf-8",
xml_declaration=True
))
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Failed to save project."
), nwAlert.ERROR, exception=exc)
return False
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
try:
if os.path.isfile(saveFile):
os.replace(saveFile, backFile)
os.replace(tempFile, saveFile)
except OSError as exc:
self.mainGui.makeAlert(self.tr(
"Failed to save project."
), nwAlert.ERROR, exception=exc)
content = self._projTree.pack()
xmlWriter = ProjectXMLWriter(self.projPath)
if not xmlWriter.write(self._data, content, saveTime, editTime):
self.mainGui.makeAlert(self.tr("Failed to save project."), nwAlert.ERROR)
return False
# Save project GUI options
@@ -1165,28 +1094,6 @@ class NWProject:
return False
return True
def _packProjectValue(self, xParent, theName, theValue, allowNone=True):
"""Pack a list of values into an xml element.
"""
if not isinstance(theValue, list):
theValue = [theValue]
for aValue in theValue:
if (aValue == "" or aValue is None) and not allowNone:
continue
xItem = etree.SubElement(xParent, theName)
xItem.text = str(aValue)
return
def _packProjectKeyValue(self, xParent, theName, theDict):
"""Pack the entries of a dictionary into an xml element.
"""
xAutoRep = etree.SubElement(xParent, theName)
for aKey, aValue in theDict.items():
if len(aKey) > 0:
xEntry = etree.SubElement(xAutoRep, "entry", attrib={"key": aKey})
xEntry.text = aValue
return
def _scanProjectFolder(self):
"""Scan the project folder and check that the files in it are
also in the project XML file. If they aren't, import them as
+155 -19
View File
@@ -26,16 +26,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import logging
import novelwriter
from enum import Enum
from lxml import etree
from novelwriter.common import (
checkBool, checkInt, checkStringNone, simplified, checkString
checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
)
from novelwriter.constants import nwFiles
logger = logging.getLogger(__name__)
FILE_VERSION = "1.4" # The current project file format version
NUM_VERSION = {
"1.0": 0x0100,
@@ -62,6 +65,29 @@ class XMLReadState(Enum):
class ProjectXMLReader:
"""The main project XML file reader class. All data is read into a
NWProjectData instance, which must be provided.
Version Change History
======================
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. Introduced in version 0.7.
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 2.0.
"""
def __init__(self, path):
@@ -86,30 +112,44 @@ class ProjectXMLReader:
@property
def content(self):
"""The project content section, a dictionary of project items.
"""
return self._content
@property
def state(self):
"""The state of the parsing as an XMLReadState enum value.
"""
return self._state
@property
def xmlRoot(self):
"""The root tag name of the XNL file,
"""
return self._root
@property
def xmlVersion(self):
"""The project XML version number.
"""
return self._version
@property
def appVersion(self):
"""The novelWriter version number who wrote the file.
"""
return self._appVersion
@property
def hexVersion(self):
"""The novelWriter version number who wrote the file as hex.
"""
return self._hexVersion
@property
def timeStamp(self):
"""The date and time when the file was written.
"""
return self._timeStamp
##
@@ -149,22 +189,6 @@ class ProjectXMLReader:
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]
@@ -425,11 +449,123 @@ class ProjectXMLWriter:
return
def write(self):
return
def write(self, projData, projContent, saveTime, editTime):
nwXML = etree.Element("novelWriterXML", attrib={
"appVersion": str(novelwriter.__version__),
"hexVersion": str(novelwriter.__hexversion__),
"fileVersion": FILE_VERSION,
"timeStamp": formatTimeStamp(saveTime),
})
# Save Project Meta
xProject = etree.SubElement(nwXML, "project")
self._packSingleValue(xProject, "name", projData.name)
self._packSingleValue(xProject, "title", projData.title)
self._packListValue(xProject, "author", projData.authors)
self._packSingleValue(xProject, "saveCount", projData.saveCount)
self._packSingleValue(xProject, "autoCount", projData.autoCount)
self._packSingleValue(xProject, "editTime", editTime)
# Save Project Settings
xSettings = etree.SubElement(nwXML, "settings")
self._packSingleValue(xSettings, "doBackup", projData.doBackup)
self._packSingleValue(xSettings, "language", projData.language)
self._packSingleValue(xSettings, "spellCheck", projData.spellCheck)
self._packSingleValue(xSettings, "spellLang", projData.spellLang)
self._packSingleValue(xSettings, "lastEdited", projData.getLastHandle("editor"))
self._packSingleValue(xSettings, "lastViewed", projData.getLastHandle("viewer"))
self._packSingleValue(xSettings, "lastNovel", projData.getLastHandle("noveltree"))
self._packSingleValue(xSettings, "lastOutline", projData.getLastHandle("outline"))
self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total"))
self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel"))
self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes"))
self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace)
self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat)
# Save Status/Importance
xStatus = etree.SubElement(xSettings, "status")
for (label, attr) in projData.itemStatus.pack():
xEntry = etree.SubElement(xStatus, "entry", attrib=attr)
xEntry.text = label
xImport = etree.SubElement(xSettings, "importance")
for (label, attr) in projData.itemImport.pack():
xEntry = etree.SubElement(xImport, "entry", attrib=attr)
xEntry.text = label
# Save Tree Content
cAttr = {"count": str(len(projContent))}
xContent = etree.SubElement(nwXML, "content", attrib=cAttr)
for item in projContent:
xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
xName.text = item["name"]
# Write the xml tree to file
tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
saveFile = os.path.join(self._path, nwFiles.PROJ_FILE)
backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak")
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
nwXML,
pretty_print=True,
encoding="utf-8",
xml_declaration=True
))
except Exception:
return False
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
try:
if os.path.isfile(saveFile):
os.replace(saveFile, backFile)
os.replace(tempFile, saveFile)
except OSError:
return False
return True
##
# Internal Functions
##
def _packSingleValue(self, xParent, name, value, allowNone=True):
"""Pack a list of values into an xml element.
"""
if (value == "" or value is None) and not allowNone:
return
xItem = etree.SubElement(xParent, name)
xItem.text = str(value)
return
def _packListValue(self, xParent, name, data, allowNone=True):
"""Pack a list of values into an xml element.
"""
for value in data:
self._packSingleValue(xParent, name, value, allowNone=allowNone)
return
def _packDictKeyValue(self, xParent, name, data):
"""Pack the entries of a dictionary into an xml element.
"""
xItem = etree.SubElement(xParent, name)
for key, value in data.items():
if len(key) > 0:
xEntry = etree.SubElement(xItem, "entry", attrib={"key": key})
xEntry.text = value
return
def _packDictTagValue(self, xParent, name, data):
"""Pack the entries of a dictionary into an xml element.
"""
xItem = etree.SubElement(xParent, name)
for aKey, value in data.items():
if len(aKey) > 0:
self._packSingleValue(xItem, aKey, value)
return
# END Class ProjectXMLWriter
+13 -15
View File
@@ -28,8 +28,6 @@ import random
import logging
import novelwriter
from lxml import etree
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
from PyQt5.QtCore import QRectF, Qt
@@ -204,21 +202,21 @@ class NWStatus:
self._store[key]["count"] += 1
return
def packXML(self, xParent):
"""Pack the status entries into an XML object for saving to the
main project file.
def pack(self):
"""Pack the status entries into a dictionary.
"""
result = []
for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={
"key": key,
"count": str(data["count"]),
"red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
})
xSub.text = data["name"]
return True
result. append((
data["name"], {
"key": key,
"count": str(data["count"]),
"red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
}
))
return result
def unpack(self, data):
"""Unpack a data dictionary and set the class values.
+5 -8
View File
@@ -27,8 +27,6 @@ import os
import random
import logging
from lxml import etree
from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkHandle
@@ -114,17 +112,16 @@ class NWTree:
return True
def packXML(self, xParent):
def pack(self):
"""Pack the content of the tree into the provided XML object. In
the order defined by the _treeOrder list.
"""
xContent = etree.SubElement(xParent, "content", attrib={
"count": str(len(self._treeOrder))}
)
tree = []
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
tItem.packXML(xContent)
return
if tItem:
tree.append(tItem.pack())
return tree
def unpack(self, data):
"""Iterate through all items of a list and add them to the
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-30 23:47:47">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-31 20:02:22">
<project>
<name>New Project</name>
<title>New Novel</title>
@@ -59,7 +59,7 @@
<name status="s000000" import="i000004">World</name>
</item>
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" heading="H0" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<meta expanded="False" heading="H1" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">Title Page</name>
</item>
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
@@ -67,11 +67,11 @@
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" heading="H0" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<meta expanded="False" heading="H2" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">New Chapter</name>
</item>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" heading="H0" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<meta expanded="False" heading="H3" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">New Scene</name>
</item>
<item handle="0000000000020" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-30 23:48:41">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-31 20:02:21">
<project>
<name>New Project</name>
<title>New Novel</title>
@@ -59,7 +59,7 @@
<name status="s000000" import="i000004">World</name>
</item>
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" heading="H0" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<meta expanded="False" heading="H1" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">Title Page</name>
</item>
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
@@ -67,11 +67,11 @@
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" heading="H0" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<meta expanded="False" heading="H2" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">New Chapter</name>
</item>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" heading="H0" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<meta expanded="False" heading="H3" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">New Scene</name>
</item>
<item handle="0000000000020" parent="None" root="0000000000020" order="0" type="ROOT" class="NOVEL">
-26
View File
@@ -22,7 +22,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
from lxml import etree
from shutil import copyfile
from zipfile import ZipFile
@@ -1033,31 +1032,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
"%s %s 200 100 99\n"
) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600))
# Pack XML Value
xElem = etree.Element("element")
theProject._packProjectValue(xElem, "A", "B", allowNone=False)
assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == (
b"<element><A>B</A></element>"
)
xElem = etree.Element("element")
theProject._packProjectValue(xElem, "A", "", allowNone=False)
assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == (
b"<element/>"
)
# Pack XML Key/Value
xElem = etree.Element("element")
theProject._packProjectKeyValue(xElem, "item", {"A": "B", "C": "D"})
assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == (
b"<element>"
b"<item>"
b"<entry key=\"A\">B</entry>"
b"<entry key=\"C\">D</entry>"
b"</item>"
b"</element>"
)
# END Test testCoreProject_Methods