Replace lxml with standard xml for project files
This commit is contained in:
@@ -27,6 +27,7 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -562,3 +563,47 @@ class NWConfigParser(ConfigParser):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# END Class NWConfigParser
|
# END Class NWConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================================== #
|
||||||
|
# Third Party Code
|
||||||
|
# =============================================================================================== #
|
||||||
|
|
||||||
|
def xmlIndent(tree, space=" ", level=0):
|
||||||
|
"""The XML indent function from CPython, that was only added in Python 3.9.
|
||||||
|
It is included here to support older versions of Python.
|
||||||
|
https://github.com/python/cpython/blob/main/Lib/xml/etree/ElementTree.py
|
||||||
|
"""
|
||||||
|
if isinstance(tree, ET.ElementTree):
|
||||||
|
tree = tree.getroot()
|
||||||
|
if level < 0:
|
||||||
|
raise ValueError(f"Initial indentation level must be >= 0, got {level}")
|
||||||
|
if not len(tree):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reduce the memory consumption by reusing indentation strings.
|
||||||
|
indentations = ["\n" + level * space]
|
||||||
|
|
||||||
|
def _indent_children(elem, level):
|
||||||
|
# Start a new indentation level for the first child.
|
||||||
|
child_level = level + 1
|
||||||
|
try:
|
||||||
|
child_indentation = indentations[child_level]
|
||||||
|
except IndexError:
|
||||||
|
child_indentation = indentations[level] + space
|
||||||
|
indentations.append(child_indentation)
|
||||||
|
|
||||||
|
if not elem.text or not elem.text.strip():
|
||||||
|
elem.text = child_indentation
|
||||||
|
|
||||||
|
for child in elem:
|
||||||
|
if len(child):
|
||||||
|
_indent_children(child, child_level)
|
||||||
|
if not child.tail or not child.tail.strip():
|
||||||
|
child.tail = child_indentation
|
||||||
|
|
||||||
|
# Dedent after the last child by overwriting the previous indentation.
|
||||||
|
if not child.tail.strip(): # type: ignore
|
||||||
|
child.tail = indentations[level] # type: ignore
|
||||||
|
|
||||||
|
_indent_children(tree, 0)
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ class Config:
|
|||||||
|
|
||||||
# Check Python Version
|
# Check Python Version
|
||||||
self.verPyString = sys.version.split()[0]
|
self.verPyString = sys.version.split()[0]
|
||||||
self.verPyHexVal = sys.hexversion
|
|
||||||
|
|
||||||
# Check OS Type
|
# Check OS Type
|
||||||
self.osType = sys.platform
|
self.osType = sys.platform
|
||||||
|
|||||||
@@ -25,17 +25,18 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from lxml import etree
|
|
||||||
from time import time
|
from time import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter import __version__, __hexversion__
|
from novelwriter import __version__, __hexversion__
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
||||||
hexToInt, simplified, yesNo
|
hexToInt, simplified, xmlIndent, yesNo
|
||||||
)
|
)
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ class ProjectXMLReader:
|
|||||||
logger.debug("Reading project XML")
|
logger.debug("Reading project XML")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml = etree.parse(str(self._path))
|
xml = ET.parse(str(self._path))
|
||||||
self._state = XMLReadState.NO_ERROR
|
self._state = XMLReadState.NO_ERROR
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -174,7 +175,7 @@ class ProjectXMLReader:
|
|||||||
backFile = self._path.with_suffix(".bak")
|
backFile = self._path.with_suffix(".bak")
|
||||||
if backFile.is_file():
|
if backFile.is_file():
|
||||||
try:
|
try:
|
||||||
xml = etree.parse(str(backFile))
|
xml = ET.parse(str(backFile))
|
||||||
self._state = XMLReadState.PARSED_BACKUP
|
self._state = XMLReadState.PARSED_BACKUP
|
||||||
logger.info("Backup project file parsed")
|
logger.info("Backup project file parsed")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -500,7 +501,7 @@ class ProjectXMLWriter:
|
|||||||
tStart = time()
|
tStart = time()
|
||||||
logger.debug("Writing project XML")
|
logger.debug("Writing project XML")
|
||||||
|
|
||||||
xRoot = etree.Element("novelWriterXML", attrib={
|
xRoot = ET.Element("novelWriterXML", attrib={
|
||||||
"appVersion": str(__version__),
|
"appVersion": str(__version__),
|
||||||
"hexVersion": str(__hexversion__),
|
"hexVersion": str(__hexversion__),
|
||||||
"fileVersion": FILE_VERSION,
|
"fileVersion": FILE_VERSION,
|
||||||
@@ -515,13 +516,13 @@ class ProjectXMLWriter:
|
|||||||
"editTime": str(editTime),
|
"editTime": str(editTime),
|
||||||
}
|
}
|
||||||
|
|
||||||
xProject = etree.SubElement(xRoot, "project", attrib=projAttr)
|
xProject = ET.SubElement(xRoot, "project", attrib=projAttr)
|
||||||
self._packSingleValue(xProject, "name", projData.name)
|
self._packSingleValue(xProject, "name", projData.name)
|
||||||
self._packSingleValue(xProject, "title", projData.title)
|
self._packSingleValue(xProject, "title", projData.title)
|
||||||
self._packSingleValue(xProject, "author", projData.author)
|
self._packSingleValue(xProject, "author", projData.author)
|
||||||
|
|
||||||
# Save Project Settings
|
# Save Project Settings
|
||||||
xSettings = etree.SubElement(xRoot, "settings")
|
xSettings = ET.SubElement(xRoot, "settings")
|
||||||
self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup))
|
self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup))
|
||||||
self._packSingleValue(xSettings, "language", projData.language)
|
self._packSingleValue(xSettings, "language", projData.language)
|
||||||
self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={
|
self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={
|
||||||
@@ -532,11 +533,11 @@ class ProjectXMLWriter:
|
|||||||
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
|
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
|
||||||
|
|
||||||
# Save Status/Importance
|
# Save Status/Importance
|
||||||
xStatus = etree.SubElement(xSettings, "status")
|
xStatus = ET.SubElement(xSettings, "status")
|
||||||
for label, attrib in projData.itemStatus.pack():
|
for label, attrib in projData.itemStatus.pack():
|
||||||
self._packSingleValue(xStatus, "entry", label, attrib=attrib)
|
self._packSingleValue(xStatus, "entry", label, attrib=attrib)
|
||||||
|
|
||||||
xImport = etree.SubElement(xSettings, "importance")
|
xImport = ET.SubElement(xSettings, "importance")
|
||||||
for label, attrib in projData.itemImport.pack():
|
for label, attrib in projData.itemImport.pack():
|
||||||
self._packSingleValue(xImport, "entry", label, attrib=attrib)
|
self._packSingleValue(xImport, "entry", label, attrib=attrib)
|
||||||
|
|
||||||
@@ -547,11 +548,11 @@ class ProjectXMLWriter:
|
|||||||
"notesWords": str(projData.currCounts[1]),
|
"notesWords": str(projData.currCounts[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
xContent = etree.SubElement(xRoot, "content", attrib=contAttr)
|
xContent = ET.SubElement(xRoot, "content", attrib=contAttr)
|
||||||
for item in projContent:
|
for item in projContent:
|
||||||
xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
|
xItem = ET.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
|
||||||
etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
|
ET.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
|
||||||
xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
|
xName = ET.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
|
||||||
xName.text = item["name"]
|
xName.text = item["name"]
|
||||||
|
|
||||||
# Write the XML tree to file
|
# Write the XML tree to file
|
||||||
@@ -559,9 +560,12 @@ class ProjectXMLWriter:
|
|||||||
tempFile = saveFile.with_suffix(".tmp")
|
tempFile = saveFile.with_suffix(".tmp")
|
||||||
backFile = saveFile.with_suffix(".bak")
|
backFile = saveFile.with_suffix(".bak")
|
||||||
try:
|
try:
|
||||||
tempFile.write_bytes(etree.tostring(
|
xml = ET.ElementTree(xRoot)
|
||||||
xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
|
if sys.hexversion < 0x030900f0:
|
||||||
))
|
xmlIndent(xml, space=" ")
|
||||||
|
else:
|
||||||
|
ET.indent(xml, space=" ")
|
||||||
|
xml.write(tempFile, encoding="utf-8", xml_declaration=True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._error = exc
|
self._error = exc
|
||||||
return False
|
return False
|
||||||
@@ -587,17 +591,17 @@ class ProjectXMLWriter:
|
|||||||
def _packSingleValue(self, xParent, name, value, attrib=None):
|
def _packSingleValue(self, xParent, name, value, attrib=None):
|
||||||
"""Pack a single value into an XML element.
|
"""Pack a single value into an XML element.
|
||||||
"""
|
"""
|
||||||
xItem = etree.SubElement(xParent, name, attrib=attrib)
|
xItem = ET.SubElement(xParent, name, attrib=attrib or {})
|
||||||
xItem.text = str(value) or ""
|
xItem.text = str(value) or ""
|
||||||
return
|
return
|
||||||
|
|
||||||
def _packDictKeyValue(self, xParent, name, data):
|
def _packDictKeyValue(self, xParent, name, data):
|
||||||
"""Pack the entries of a dictionary into an XML element.
|
"""Pack the entries of a dictionary into an XML element.
|
||||||
"""
|
"""
|
||||||
xItem = etree.SubElement(xParent, name)
|
xItem = ET.SubElement(xParent, name)
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if len(key) > 0:
|
if len(key) > 0:
|
||||||
xEntry = etree.SubElement(xItem, "entry", attrib={"key": key})
|
xEntry = ET.SubElement(xItem, "entry", attrib={"key": key})
|
||||||
xEntry.text = str(value) or ""
|
xEntry.text = str(value) or ""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -31,10 +32,10 @@ from pathlib import Path
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
|
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
|
||||||
from PyQt5.QtGui import QIcon, QKeySequence, QCursor
|
from PyQt5.QtGui import QCursor, QIcon, QKeySequence
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
|
qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter,
|
||||||
QMessageBox, QDialog, QStackedWidget
|
QStackedWidget, QVBoxLayout, QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, __hexversion__
|
from novelwriter import CONFIG, __hexversion__
|
||||||
@@ -88,7 +89,7 @@ class GuiMain(QMainWindow):
|
|||||||
logger.info("Host: %s", CONFIG.hostName)
|
logger.info("Host: %s", CONFIG.hostName)
|
||||||
logger.info("Qt5: %s (0x%06x)", CONFIG.verQtString, CONFIG.verQtValue)
|
logger.info("Qt5: %s (0x%06x)", CONFIG.verQtString, CONFIG.verQtValue)
|
||||||
logger.info("PyQt5: %s (0x%06x)", CONFIG.verPyQtString, CONFIG.verPyQtValue)
|
logger.info("PyQt5: %s (0x%06x)", CONFIG.verPyQtString, CONFIG.verPyQtValue)
|
||||||
logger.info("Python: %s (0x%08x)", CONFIG.verPyString, CONFIG.verPyHexVal)
|
logger.info("Python: %s (0x%08x)", CONFIG.verPyString, sys.hexversion)
|
||||||
logger.info("GUI Language: %s", CONFIG.guiLocale)
|
logger.info("GUI Language: %s", CONFIG.guiLocale)
|
||||||
|
|
||||||
# Core Classes
|
# Core Classes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" timeStamp="2023-04-15 20:52:06">
|
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" timeStamp="2023-05-29 16:50:14">
|
||||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1475" autoCount="237" editTime="74579">
|
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1483" autoCount="237" editTime="74603">
|
||||||
<name>Sample Project</name>
|
<name>Sample Project</name>
|
||||||
<title>Sample Project</title>
|
<title>Sample Project</title>
|
||||||
<author>Jane Smith</author>
|
<author>Jane Smith</author>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<entry key="chapter">Chapter %chw%: %title%</entry>
|
<entry key="chapter">Chapter %chw%: %title%</entry>
|
||||||
<entry key="unnumbered">%title%</entry>
|
<entry key="unnumbered">%title%</entry>
|
||||||
<entry key="scene">Scene %ch%.%sc%: %title%</entry>
|
<entry key="scene">Scene %ch%.%sc%: %title%</entry>
|
||||||
<entry key="section"></entry>
|
<entry key="section" />
|
||||||
</titleFormat>
|
</titleFormat>
|
||||||
<status>
|
<status>
|
||||||
<entry key="sf12341" count="4" red="100" green="100" blue="100">New</entry>
|
<entry key="sf12341" count="4" red="100" green="100" blue="100">New</entry>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="67"/>
|
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="1090" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
|
|||||||
Reference in New Issue
Block a user