diff --git a/novelwriter/common.py b/novelwriter/common.py
index 91058fd0..f84f22ac 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -27,6 +27,7 @@ import json
import uuid
import hashlib
import logging
+import xml.etree.ElementTree as ET
from pathlib import Path
from datetime import datetime
@@ -562,3 +563,47 @@ class NWConfigParser(ConfigParser):
return result
# 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)
diff --git a/novelwriter/config.py b/novelwriter/config.py
index 89019ce4..68f44ec4 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -196,7 +196,6 @@ class Config:
# Check Python Version
self.verPyString = sys.version.split()[0]
- self.verPyHexVal = sys.hexversion
# Check OS Type
self.osType = sys.platform
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 581a0ef7..a103774b 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -25,17 +25,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import sys
import logging
+import xml.etree.ElementTree as ET
from enum import Enum
-from lxml import etree
from time import time
from pathlib import Path
from novelwriter import __version__, __hexversion__
from novelwriter.common import (
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
- hexToInt, simplified, yesNo
+ hexToInt, simplified, xmlIndent, yesNo
)
from novelwriter.constants import nwFiles
@@ -163,7 +164,7 @@ class ProjectXMLReader:
logger.debug("Reading project XML")
try:
- xml = etree.parse(str(self._path))
+ xml = ET.parse(str(self._path))
self._state = XMLReadState.NO_ERROR
except Exception as exc:
@@ -174,7 +175,7 @@ class ProjectXMLReader:
backFile = self._path.with_suffix(".bak")
if backFile.is_file():
try:
- xml = etree.parse(str(backFile))
+ xml = ET.parse(str(backFile))
self._state = XMLReadState.PARSED_BACKUP
logger.info("Backup project file parsed")
except Exception as exc:
@@ -500,7 +501,7 @@ class ProjectXMLWriter:
tStart = time()
logger.debug("Writing project XML")
- xRoot = etree.Element("novelWriterXML", attrib={
+ xRoot = ET.Element("novelWriterXML", attrib={
"appVersion": str(__version__),
"hexVersion": str(__hexversion__),
"fileVersion": FILE_VERSION,
@@ -515,13 +516,13 @@ class ProjectXMLWriter:
"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, "title", projData.title)
self._packSingleValue(xProject, "author", projData.author)
# Save Project Settings
- xSettings = etree.SubElement(xRoot, "settings")
+ xSettings = ET.SubElement(xRoot, "settings")
self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup))
self._packSingleValue(xSettings, "language", projData.language)
self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={
@@ -532,11 +533,11 @@ class ProjectXMLWriter:
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
# Save Status/Importance
- xStatus = etree.SubElement(xSettings, "status")
+ xStatus = ET.SubElement(xSettings, "status")
for label, attrib in projData.itemStatus.pack():
self._packSingleValue(xStatus, "entry", label, attrib=attrib)
- xImport = etree.SubElement(xSettings, "importance")
+ xImport = ET.SubElement(xSettings, "importance")
for label, attrib in projData.itemImport.pack():
self._packSingleValue(xImport, "entry", label, attrib=attrib)
@@ -547,11 +548,11 @@ class ProjectXMLWriter:
"notesWords": str(projData.currCounts[1]),
}
- xContent = etree.SubElement(xRoot, "content", attrib=contAttr)
+ xContent = ET.SubElement(xRoot, "content", attrib=contAttr)
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", {}))
+ xItem = ET.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
+ ET.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
+ xName = ET.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
xName.text = item["name"]
# Write the XML tree to file
@@ -559,9 +560,12 @@ class ProjectXMLWriter:
tempFile = saveFile.with_suffix(".tmp")
backFile = saveFile.with_suffix(".bak")
try:
- tempFile.write_bytes(etree.tostring(
- xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
- ))
+ xml = ET.ElementTree(xRoot)
+ 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:
self._error = exc
return False
@@ -587,17 +591,17 @@ class ProjectXMLWriter:
def _packSingleValue(self, xParent, name, value, attrib=None):
"""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 ""
return
def _packDictKeyValue(self, xParent, name, data):
"""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():
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 ""
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 6add5bac..89393085 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -23,6 +23,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import sys
import logging
from enum import Enum
@@ -31,10 +32,10 @@ from pathlib import Path
from datetime import datetime
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 (
- qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
- QMessageBox, QDialog, QStackedWidget
+ qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter,
+ QStackedWidget, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, __hexversion__
@@ -88,7 +89,7 @@ class GuiMain(QMainWindow):
logger.info("Host: %s", CONFIG.hostName)
logger.info("Qt5: %s (0x%06x)", CONFIG.verQtString, CONFIG.verQtValue)
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)
# Core Classes
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 1e51d52f..96e22a4a 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
@@ -25,7 +25,7 @@
Chapter %chw%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,112 +45,112 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Page
-
-
+
Part One
-
-
+
Chapter One
-
-
+
Making a Scene
-
-
+
Another Scene
-
-
+
Interlude
-
-
+
A Note on Structure
-
-
+
Chapter Two
-
-
+
We Found John!
-
-
+
Sequel
-
-
+
Title Page
-
-
+
Chapter One
-
-
+
Characters
-
-
+
Main Characters
-
-
+
John Smith
-
-
+
Jane Smith
-
-
+
Locations
-
-
+
Earth
-
-
+
Space
-
-
+
Mars
-
-
+
Archive
-
-
+
Scenes
-
-
+
Old File
-
-
+
Trash
-
-
+
Delete Me!
-
+
\ No newline at end of file