From 59931c804c342c31fb117766ecd4c459d22c3ac6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 17:00:52 +0200
Subject: [PATCH 01/11] Replace lxml with standard xml for project files
---
novelwriter/common.py | 45 ++++++++++++++++++++++++
novelwriter/config.py | 1 -
novelwriter/core/projectxml.py | 42 ++++++++++++-----------
novelwriter/guimain.py | 9 ++---
sample/nwProject.nwx | 62 +++++++++++++++++-----------------
5 files changed, 104 insertions(+), 55 deletions(-)
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
From af67f5081e7f48cc47720ef029336988ff169584 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 17:16:58 +0200
Subject: [PATCH 02/11] Fix test reference files
---
tests/lipsum/nwProject.nwx | 50 ++++++++---------
.../coreProject_NewFileFolder_nwProject.nwx | 28 +++++-----
.../coreProject_NewRoot_nwProject.nwx | 38 ++++++-------
.../coreTools_NewCustomA_nwProject.nwx | 50 ++++++++---------
.../coreTools_NewCustomB_nwProject.nwx | 38 ++++++-------
.../coreTools_NewMinimal_nwProject.nwx | 24 ++++----
.../guiEditor_Main_Final_nwProject.nwx | 28 +++++-----
.../guiEditor_Main_Initial_nwProject.nwx | 22 ++++----
tests/reference/projectXML_ReadLegacy10.nwx | 46 +++++++--------
tests/reference/projectXML_ReadLegacy11.nwx | 46 +++++++--------
tests/reference/projectXML_ReadLegacy12.nwx | 52 ++++++++---------
tests/reference/projectXML_ReadLegacy13.nwx | 52 ++++++++---------
tests/reference/projectXML_ReadLegacy14.nwx | 56 +++++++++----------
tests/tools.py | 2 +-
14 files changed, 266 insertions(+), 266 deletions(-)
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index 086183c6..0a4a69e5 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
Lorem Ipsum
Lorem Ipsum
lipsum.com
@@ -24,7 +24,7 @@
Chapter %ch%: %title%
%title%
* * *
-
+
New
@@ -41,88 +41,88 @@
-
-
+
Novel
-
-
+
Lorem Ipsum
-
-
+
Front Matter
-
-
+
Prologue
-
-
+
Act One
-
-
+
Chapter One
-
-
+
Chapter One
-
-
+
Scene One
-
-
+
Scene Two
-
-
+
Interlude
-
-
+
Chapter Two
-
-
+
Chapter Two
-
-
+
Scene Three
-
-
+
Scene Four
-
-
+
Scene Five
-
-
+
Characters
-
-
+
Mr. Nobody
-
-
+
Plot
-
-
+
Main
-
-
+
World
-
-
+
Ancient Europe
-
+
\ No newline at end of file
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 5000c7d9..c80520fa 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -15,13 +15,13 @@
None
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,47 +38,47 @@
-
-
+
Novel
-
-
+
Plot
-
-
+
Characters
-
-
+
World
-
-
+
Title Page
-
-
+
New Chapter
-
-
+
New Chapter
-
-
+
New Scene
-
-
+
Stuff
-
-
+
Hello
-
-
+
Jane
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 06a0ca07..c94ce28e 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -15,13 +15,13 @@
None
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,67 +38,67 @@
-
-
+
Novel
-
-
+
Plot
-
-
+
Characters
-
-
+
World
-
-
+
Title Page
-
-
+
New Chapter
-
-
+
New Chapter
-
-
+
New Scene
-
-
+
Novel
-
-
+
Plot
-
-
+
Characters
-
-
+
Locations
-
-
+
Timeline
-
-
+
Objects
-
-
+
Custom
-
-
+
Custom
diff --git a/tests/reference/coreTools_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx
index ffa4f38a..84419675 100644
--- a/tests/reference/coreTools_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -15,13 +15,13 @@
None
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,91 +38,91 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Chapter 1
-
-
+
Scene 1.1
-
-
+
Scene 1.2
-
-
+
Scene 1.3
-
-
+
Chapter 2
-
-
+
Scene 2.1
-
-
+
Scene 2.2
-
-
+
Scene 2.3
-
-
+
Chapter 3
-
-
+
Scene 3.1
-
-
+
Scene 3.2
-
-
+
Scene 3.3
-
-
+
Plot
-
-
+
Main Plot
-
-
+
Characters
-
-
+
Protagonist
-
-
+
Locations
-
-
+
Main Location
-
-
+
Archive
-
-
+
Trash
diff --git a/tests/reference/coreTools_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx
index 38084419..53e23cd8 100644
--- a/tests/reference/coreTools_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -15,13 +15,13 @@
None
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,67 +38,67 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Scene 1
-
-
+
Scene 2
-
-
+
Scene 3
-
-
+
Scene 4
-
-
+
Scene 5
-
-
+
Scene 6
-
-
+
Plot
-
-
+
Main Plot
-
-
+
Characters
-
-
+
Protagonist
-
-
+
Locations
-
-
+
Main Location
-
-
+
Archive
-
-
+
Trash
diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx
index dc3f2d43..04ad571c 100644
--- a/tests/reference/coreTools_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx
@@ -1,9 +1,9 @@
-
+
New Project
New Project
-
+
yes
@@ -15,13 +15,13 @@
None
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,35 +38,35 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
New Chapter
-
-
+
New Scene
-
-
+
Plot
-
-
+
Characters
-
-
+
Locations
-
-
+
Archive
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index dc0456a7..c5c3b367 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -15,13 +15,13 @@
0000000000008
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,47 +38,47 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
New Chapter
-
-
+
New Chapter
-
-
+
New Scene
-
-
+
Plot
-
-
+
New Note
-
-
+
Characters
-
-
+
New Note
-
-
+
World
-
-
+
New Note
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index 977f03c1..4d758791 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -15,13 +15,13 @@
None
None
-
+
%title%
%title%
%title%
* * *
-
+
New
@@ -38,35 +38,35 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
New Chapter
-
-
+
New Chapter
-
-
+
New Scene
-
-
+
Plot
-
-
+
Characters
-
-
+
World
diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx
index 5b189c1e..edcab5b4 100644
--- a/tests/reference/projectXML_ReadLegacy10.nwx
+++ b/tests/reference/projectXML_ReadLegacy10.nwx
@@ -25,7 +25,7 @@
Chapter %ch%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,91 +45,91 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Page
-
-
+
Part One
-
-
+
A Folder
-
-
+
Chapter One
-
-
+
Making a Scene
-
-
+
Another Scene
-
-
+
Interlude
-
-
+
A Note on Structure
-
-
+
Chapter Two
-
-
+
We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
+
John Smith
-
-
+
Jane Smith
-
-
+
Locations
-
-
+
Earth
-
-
+
Space
-
-
+
Mars
-
-
+
Trash
-
-
+
Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx
index afc87cef..fcc987ef 100644
--- a/tests/reference/projectXML_ReadLegacy11.nwx
+++ b/tests/reference/projectXML_ReadLegacy11.nwx
@@ -25,7 +25,7 @@
Chapter %ch%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,91 +45,91 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Page
-
-
+
Part One
-
-
+
A Folder
-
-
+
Chapter One
-
-
+
Making a Scene
-
-
+
Another Scene
-
-
+
Interlude
-
-
+
A Note on Structure
-
-
+
Chapter Two
-
-
+
We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
+
John Smith
-
-
+
Jane Smith
-
-
+
Locations
-
-
+
Earth
-
-
+
Space
-
-
+
Mars
-
-
+
Trash
-
-
+
Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx
index e31f035a..b44e9acf 100644
--- a/tests/reference/projectXML_ReadLegacy12.nwx
+++ b/tests/reference/projectXML_ReadLegacy12.nwx
@@ -25,7 +25,7 @@
Chapter %chw%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,103 +45,103 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Page
-
-
+
Part One
-
-
+
A Folder
-
-
+
Chapter One
-
-
+
Making a Scene
-
-
+
Another Scene
-
-
+
Interlude
-
-
+
A Note on Structure
-
-
+
Chapter Two
-
-
+
We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
+
John Smith
-
-
+
Jane Smith
-
-
+
Locations
-
-
+
Earth
-
-
+
Space
-
-
+
Mars
-
-
+
Outtakes
-
-
+
Scenes
-
-
+
Old File
-
-
+
Trash
-
-
+
Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx
index d629079d..06fc3145 100644
--- a/tests/reference/projectXML_ReadLegacy13.nwx
+++ b/tests/reference/projectXML_ReadLegacy13.nwx
@@ -25,7 +25,7 @@
Chapter %chw%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,103 +45,103 @@
-
-
+
Novel
-
-
+
Title Page
-
-
+
Page
-
-
+
Part One
-
-
+
A Folder
-
-
+
Chapter One
-
-
+
Making a Scene
-
-
+
Another Scene
-
-
+
Interlude
-
-
+
A Note on Structure
-
-
+
Chapter Two
-
-
+
We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
+
John Smith
-
-
+
Jane Smith
-
-
+
Locations
-
-
+
Earth
-
-
+
Space
-
-
+
Mars
-
-
+
Archive
-
-
+
Scenes
-
-
+
Old File
-
-
+
Trash
-
-
+
Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx
index 6bc7154c..e40e3a07 100644
--- a/tests/reference/projectXML_ReadLegacy14.nwx
+++ b/tests/reference/projectXML_ReadLegacy14.nwx
@@ -25,7 +25,7 @@
Chapter %chw%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,111 +45,111 @@
-
-
+
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!
diff --git a/tests/tools.py b/tests/tools.py
index a9933bc4..7463e3a1 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -27,7 +27,7 @@ from pathlib import Path
from PyQt5.QtWidgets import qApp
XML_IGNORE = ("
Date: Mon, 29 May 2023 17:58:23 +0200
Subject: [PATCH 03/11] Fix last broken test
---
novelwriter/common.py | 2 +-
tests/files/nwProject-1.5.nwx | 56 ++++++++++++-------------
tests/test_core/test_core_projectxml.py | 2 +-
3 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index f84f22ac..f4161a31 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -569,7 +569,7 @@ class NWConfigParser(ConfigParser):
# Third Party Code
# =============================================================================================== #
-def xmlIndent(tree, space=" ", level=0):
+def xmlIndent(tree, space=" ", level=0): # pragma: no cover
"""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
diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx
index 8bee81e4..8952b63b 100644
--- a/tests/files/nwProject-1.5.nwx
+++ b/tests/files/nwProject-1.5.nwx
@@ -25,7 +25,7 @@
Chapter %chw%: %title%
%title%
Scene %ch%.%sc%: %title%
-
+
New
@@ -45,111 +45,111 @@
-
-
+
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!
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 4d099632..35132107 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -226,7 +226,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
# Fail saving
with monkeypatch.context() as mp:
- mp.setattr("pathlib.Path.write_bytes", causeOSError)
+ mp.setattr("xml.etree.ElementTree.ElementTree.write", causeOSError)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False
assert str(xmlWriter.error) == "Mock OSError"
From 47a6ae5031ff7d522a89729e0857eafef63e338f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 18:23:00 +0200
Subject: [PATCH 04/11] Remove test runs for Python 3.7
---
.github/workflows/test_linux.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml
index 346b8db5..30b50db2 100644
--- a/.github/workflows/test_linux.yml
+++ b/.github/workflows/test_linux.yml
@@ -14,7 +14,7 @@ jobs:
testLinux:
strategy:
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
runs-on: ubuntu-latest
steps:
- name: Python Setup
From 22e0f826543e9ecaa4dccd43052bd15adab5979f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 20:04:28 +0200
Subject: [PATCH 05/11] Replaxe lxml with standard library xml in ODT class
---
novelwriter/core/toodt.py | 235 ++++++++++++++++++--------------------
1 file changed, 113 insertions(+), 122 deletions(-)
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 4251d066..dd980d23 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -24,13 +24,14 @@ along with this program. If not, see .
"""
import logging
+import xml.etree.ElementTree as ET
-from lxml import etree
from hashlib import sha256
from zipfile import ZipFile
from datetime import datetime
from novelwriter import __version__
+from novelwriter.common import xmlIndent
from novelwriter.constants import nwKeyWords, nwLabels
from novelwriter.core.tokenizer import Tokenizer, stripEscape
@@ -38,30 +39,27 @@ logger = logging.getLogger(__name__)
# Main XML NameSpaces
XML_NS = {
- "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
- "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
- "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
- "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
- "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
- "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
- "dc": "http://purl.org/dc/elements/1.1/",
-}
-MANI_NS = {
- "manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
-}
-OFFICE_NS = {
- "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
+ "manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
+ "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
+ "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+ "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
+ "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
+ "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
+ "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+ "dc": "http://purl.org/dc/elements/1.1/",
}
+for ns, uri in XML_NS.items():
+ ET.register_namespace(ns, uri)
-def _mkTag(nsName, tagName, nsMap=XML_NS):
+def _mkTag(ns, tag):
"""Assemble namespace and tag name.
"""
- theNS = nsMap.get(nsName, "")
- if theNS:
- return f"{{{theNS}}}{tagName}"
- logger.warning("Missing xml namespace '%s'", nsName)
- return tagName
+ uri = XML_NS.get(ns, "")
+ if uri:
+ return f"{{{uri}}}{tag}"
+ logger.warning("Missing xml namespace '%s'", ns)
+ return tag
# Mimetype and Version
@@ -284,16 +282,16 @@ class ToOdt(Tokenizer):
tAttr[_mkTag("office", "mimetype")] = X_MIME
tFlat = _mkTag("office", "document")
- self._dFlat = etree.Element(tFlat, attrib=tAttr, nsmap=XML_NS)
+ self._dFlat = ET.Element(tFlat, attrib=tAttr)
- self._xMeta = etree.SubElement(self._dFlat, _mkTag("office", "meta"))
- self._xFont = etree.SubElement(self._dFlat, _mkTag("office", "font-face-decls"))
- self._xStyl = etree.SubElement(self._dFlat, _mkTag("office", "styles"))
- self._xAuto = etree.SubElement(self._dFlat, _mkTag("office", "automatic-styles"))
- self._xMast = etree.SubElement(self._dFlat, _mkTag("office", "master-styles"))
- self._xBody = etree.SubElement(self._dFlat, _mkTag("office", "body"))
+ self._xMeta = ET.SubElement(self._dFlat, _mkTag("office", "meta"))
+ self._xFont = ET.SubElement(self._dFlat, _mkTag("office", "font-face-decls"))
+ self._xStyl = ET.SubElement(self._dFlat, _mkTag("office", "styles"))
+ self._xAuto = ET.SubElement(self._dFlat, _mkTag("office", "automatic-styles"))
+ self._xMast = ET.SubElement(self._dFlat, _mkTag("office", "master-styles"))
+ self._xBody = ET.SubElement(self._dFlat, _mkTag("office", "body"))
- etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
+ ET.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
else:
@@ -305,59 +303,59 @@ class ToOdt(Tokenizer):
tStyl = _mkTag("office", "document-styles")
# content.xml
- self._dCont = etree.Element(tCont, attrib=tAttr, nsmap=XML_NS)
- self._xFont = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls"))
- self._xAuto = etree.SubElement(self._dCont, _mkTag("office", "automatic-styles"))
- self._xBody = etree.SubElement(self._dCont, _mkTag("office", "body"))
+ self._dCont = ET.Element(tCont, attrib=tAttr)
+ self._xFont = ET.SubElement(self._dCont, _mkTag("office", "font-face-decls"))
+ self._xAuto = ET.SubElement(self._dCont, _mkTag("office", "automatic-styles"))
+ self._xBody = ET.SubElement(self._dCont, _mkTag("office", "body"))
# meta.xml
- self._dMeta = etree.Element(tMeta, attrib=tAttr, nsmap=XML_NS)
- self._xMeta = etree.SubElement(self._dMeta, _mkTag("office", "meta"))
+ self._dMeta = ET.Element(tMeta, attrib=tAttr)
+ self._xMeta = ET.SubElement(self._dMeta, _mkTag("office", "meta"))
# styles.xml
- self._dStyl = etree.Element(tStyl, attrib=tAttr, nsmap=XML_NS)
- self._xFnt2 = etree.SubElement(self._dStyl, _mkTag("office", "font-face-decls"))
- self._xStyl = etree.SubElement(self._dStyl, _mkTag("office", "styles"))
- self._xAut2 = etree.SubElement(self._dStyl, _mkTag("office", "automatic-styles"))
- self._xMast = etree.SubElement(self._dStyl, _mkTag("office", "master-styles"))
+ self._dStyl = ET.Element(tStyl, attrib=tAttr)
+ self._xFnt2 = ET.SubElement(self._dStyl, _mkTag("office", "font-face-decls"))
+ self._xStyl = ET.SubElement(self._dStyl, _mkTag("office", "styles"))
+ self._xAut2 = ET.SubElement(self._dStyl, _mkTag("office", "automatic-styles"))
+ self._xMast = ET.SubElement(self._dStyl, _mkTag("office", "master-styles"))
- etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
- etree.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr)
+ ET.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
+ ET.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr)
# Finalise
# ========
- self._xText = etree.SubElement(self._xBody, _mkTag("office", "text"))
+ self._xText = ET.SubElement(self._xBody, _mkTag("office", "text"))
timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
# Office Meta Data
- xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = timeStamp
- xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{__version__}"
- xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "initial-creator"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "initial-creator"))
xMeta.text = self.theProject.data.author
- xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "editing-cycles"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "editing-cycles"))
xMeta.text = str(self.theProject.data.saveCount)
# Format is: PnYnMnDTnHnMnS
# https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#duration
eT = self.theProject.data.editTime
- xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "editing-duration"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "editing-duration"))
xMeta.text = f"P{eT//86400:d}DT{eT%86400//3600:d}H{eT%3600//60:d}M{eT%60:d}S"
# Dublin Core Meta Data
- xMeta = etree.SubElement(self._xMeta, _mkTag("dc", "title"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title"))
xMeta.text = self.theProject.data.title or self.theProject.data.name
- xMeta = etree.SubElement(self._xMeta, _mkTag("dc", "date"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date"))
xMeta.text = timeStamp
- xMeta = etree.SubElement(self._xMeta, _mkTag("dc", "creator"))
+ xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "creator"))
xMeta.text = self.theProject.data.author
self._pageStyles()
@@ -503,51 +501,44 @@ class ToOdt(Tokenizer):
"""Save the data to an .fodt file.
"""
with open(savePath, mode="wb") as outFile:
- outFile.write(etree.tostring(
- self._dFlat,
- pretty_print=True,
- encoding="utf-8",
- xml_declaration=True
- ))
+ xml = ET.ElementTree(self._dFlat)
+ xmlIndent(xml, space=" ")
+ xml.write(outFile, encoding="utf-8", xml_declaration=True)
return
def saveOpenDocText(self, savePath):
"""Save the data to an .odt file.
"""
- mMani = _mkTag("manifest", "manifest", nsMap=MANI_NS)
- mVers = _mkTag("manifest", "version", nsMap=MANI_NS)
- mPath = _mkTag("manifest", "full-path", nsMap=MANI_NS)
- mType = _mkTag("manifest", "media-type", nsMap=MANI_NS)
- mFile = _mkTag("manifest", "file-entry", nsMap=MANI_NS)
+ mMani = _mkTag("manifest", "manifest")
+ mVers = _mkTag("manifest", "version")
+ mPath = _mkTag("manifest", "full-path")
+ mType = _mkTag("manifest", "media-type")
+ mFile = _mkTag("manifest", "file-entry")
- xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=MANI_NS)
- etree.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
- etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
- etree.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
- etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
- etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
+ xMani = ET.Element(mMani, attrib={mVers: X_VERS})
+ ET.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
+ ET.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
+ ET.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
+ ET.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
+ ET.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
- oRoot = _mkTag("office", "document-settings", nsMap=OFFICE_NS)
- oVers = _mkTag("office", "version", nsMap=OFFICE_NS)
- xSett = etree.Element(oRoot, nsmap=OFFICE_NS, attrib={oVers: X_VERS})
+ oRoot = _mkTag("office", "document-settings")
+ oVers = _mkTag("office", "version")
+ xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
- with ZipFile(savePath, mode="w") as outFile:
- outFile.writestr("mimetype", X_MIME)
- outFile.writestr("META-INF/manifest.xml", etree.tostring(
- xMani, pretty_print=False, encoding="utf-8", xml_declaration=True
- ))
- outFile.writestr("settings.xml", etree.tostring(
- xSett, pretty_print=False, encoding="utf-8", xml_declaration=True
- ))
- outFile.writestr("content.xml", etree.tostring(
- self._dCont, pretty_print=False, encoding="utf-8", xml_declaration=True
- ))
- outFile.writestr("meta.xml", etree.tostring(
- self._dMeta, pretty_print=False, encoding="utf-8", xml_declaration=True
- ))
- outFile.writestr("styles.xml", etree.tostring(
- self._dStyl, pretty_print=False, encoding="utf-8", xml_declaration=True
- ))
+ def putInZip(name, xObj, zipObj):
+ with zipObj.open(name, mode="w") as fObj:
+ xml = ET.ElementTree(xObj)
+ xmlIndent(xml, space=" ")
+ xml.write(fObj, encoding="utf-8", xml_declaration=True)
+
+ with ZipFile(savePath, mode="w") as outZip:
+ outZip.writestr("mimetype", X_MIME)
+ putInZip("META-INF/manifest.xml", xMani, outZip)
+ putInZip("settings.xml", xSett, outZip)
+ putInZip("content.xml", self._dCont, outZip)
+ putInZip("meta.xml", self._dMeta, outZip)
+ putInZip("styles.xml", self._dStyl, outZip)
return
@@ -604,7 +595,7 @@ class ToOdt(Tokenizer):
tAttr[_mkTag("text", "outline-level")] = oLevel
pTag = "h" if isHead else "p"
- xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
+ xElem = ET.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
# It's important to set the initial text field to empty, otherwise
# lxml will add a line break if the first subelement is a span.
@@ -732,25 +723,25 @@ class ToOdt(Tokenizer):
theAttr = {}
theAttr[_mkTag("style", "name")] = "PM1"
if self._isFlat:
- xPage = etree.SubElement(self._xAuto, _mkTag("style", "page-layout"), attrib=theAttr)
+ xPage = ET.SubElement(self._xAuto, _mkTag("style", "page-layout"), attrib=theAttr)
else:
- xPage = etree.SubElement(self._xAut2, _mkTag("style", "page-layout"), attrib=theAttr)
+ xPage = ET.SubElement(self._xAut2, _mkTag("style", "page-layout"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("fo", "margin-top")] = self._mDocTop
theAttr[_mkTag("fo", "margin-bottom")] = self._mDocBtm
theAttr[_mkTag("fo", "margin-left")] = self._mDocLeft
theAttr[_mkTag("fo", "margin-right")] = self._mDocRight
- etree.SubElement(xPage, _mkTag("style", "page-layout-properties"), attrib=theAttr)
+ ET.SubElement(xPage, _mkTag("style", "page-layout-properties"), attrib=theAttr)
- xHead = etree.SubElement(xPage, _mkTag("style", "header-style"))
+ xHead = ET.SubElement(xPage, _mkTag("style", "header-style"))
theAttr = {}
theAttr[_mkTag("fo", "min-height")] = "0.600cm"
theAttr[_mkTag("fo", "margin-left")] = "0.000cm"
theAttr[_mkTag("fo", "margin-right")] = "0.000cm"
theAttr[_mkTag("fo", "margin-bottom")] = "0.500cm"
- etree.SubElement(xHead, _mkTag("style", "header-footer-properties"), attrib=theAttr)
+ ET.SubElement(xHead, _mkTag("style", "header-footer-properties"), attrib=theAttr)
return
@@ -762,13 +753,13 @@ class ToOdt(Tokenizer):
theAttr = {}
theAttr[_mkTag("style", "family")] = "paragraph"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr)
+ xStyl = ET.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "line-break")] = "strict"
theAttr[_mkTag("style", "tab-stop-distance")] = "1.251cm"
theAttr[_mkTag("style", "writing-mode")] = "page"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+ ET.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self._textFont
@@ -776,7 +767,7 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
theAttr[_mkTag("fo", "language")] = self._dLanguage
theAttr[_mkTag("fo", "country")] = self._dCountry
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Standard Paragraph Style
# ============================
@@ -785,13 +776,13 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("style", "name")] = "Standard"
theAttr[_mkTag("style", "family")] = "paragraph"
theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ xStyl = ET.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self._textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Default Heading Style
# =========================
@@ -802,19 +793,19 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("style", "parent-style-name")] = "Standard"
theAttr[_mkTag("style", "next-style-name")] = "Text_20_body"
theAttr[_mkTag("style", "class")] = "text"
- xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ xStyl = ET.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("fo", "margin-top")] = self._mTopHead
theAttr[_mkTag("fo", "margin-bottom")] = self._mBotHead
theAttr[_mkTag("fo", "keep-with-next")] = "always"
- etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+ ET.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self._textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeHead
- etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+ ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Header and Footer Styles
# ============================
@@ -824,7 +815,7 @@ class ToOdt(Tokenizer):
theAttr[_mkTag("style", "family")] = "paragraph"
theAttr[_mkTag("style", "parent-style-name")] = "Standard"
theAttr[_mkTag("style", "class")] = "extra"
- etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+ ET.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
return
@@ -989,23 +980,23 @@ class ToOdt(Tokenizer):
theAttr = {}
theAttr[_mkTag("style", "name")] = "Standard"
theAttr[_mkTag("style", "page-layout-name")] = "PM1"
- xPage = etree.SubElement(self._xMast, _mkTag("style", "master-page"), attrib=theAttr)
+ xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib=theAttr)
# Standard Page Header
- xHead = etree.SubElement(xPage, _mkTag("style", "header"))
- xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={
+ xHead = ET.SubElement(xPage, _mkTag("style", "header"))
+ xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
_mkTag("text", "style-name"): "Header"
})
xPar.text = self._headerText.strip() + " "
- xTail = etree.SubElement(xPar, _mkTag("text", "page-number"), attrib={
+ xTail = ET.SubElement(xPar, _mkTag("text", "page-number"), attrib={
_mkTag("text", "select-page"): "current"
})
xTail.text = "2"
# First Page Header
- xHead = etree.SubElement(xPage, _mkTag("style", "header-first"))
- xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={
+ xHead = ET.SubElement(xPage, _mkTag("style", "header-first"))
+ xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
_mkTag("text", "style-name"): "Header"
})
@@ -1209,7 +1200,7 @@ class ODTParagraphStyle:
if aVal is not None:
theAttr[_mkTag(aNm, aName)] = aVal
- xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+ xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
for aName, (aNm, aVal) in self._pAttr.items():
@@ -1217,7 +1208,7 @@ class ODTParagraphStyle:
theAttr[_mkTag(aNm, aName)] = aVal
if theAttr:
- etree.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+ ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
for aName, (aNm, aVal) in self._tAttr.items():
@@ -1225,7 +1216,7 @@ class ODTParagraphStyle:
theAttr[_mkTag(aNm, aName)] = aVal
if theAttr:
- etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+ ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
return
@@ -1296,7 +1287,7 @@ class ODTTextStyle:
theAttr = {}
theAttr[_mkTag("style", "name")] = xName
theAttr[_mkTag("style", "family")] = "text"
- xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+ xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
for aName, (aNm, aVal) in self._tAttr.items():
@@ -1304,7 +1295,7 @@ class ODTTextStyle:
theAttr[_mkTag(aNm, aName)] = aVal
if theAttr:
- etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+ ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
return
@@ -1377,26 +1368,26 @@ class XMLParagraph:
if c == "\n":
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
- self._xTail = etree.SubElement(self._xRoot, TAG_BR)
+ self._xTail = ET.SubElement(self._xRoot, TAG_BR)
self._xTail.tail = ""
self._nState = X_ROOT_TAIL
self._chrPos += 1
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
- self._xSing = etree.SubElement(self._xTail, TAG_BR)
+ self._xSing = ET.SubElement(self._xTail, TAG_BR)
self._xSing.tail = ""
self._nState = X_SPAN_SING
self._chrPos += 1
elif c == "\t":
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
- self._xTail = etree.SubElement(self._xRoot, TAG_TAB)
+ self._xTail = ET.SubElement(self._xRoot, TAG_TAB)
self._xTail.tail = ""
self._nState = X_ROOT_TAIL
self._chrPos += 1
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
- self._xSing = etree.SubElement(self._xTail, TAG_TAB)
+ self._xSing = ET.SubElement(self._xTail, TAG_TAB)
self._xSing.tail = ""
self._chrPos += 1
self._nState = X_SPAN_SING
@@ -1426,7 +1417,7 @@ class XMLParagraph:
Therefore we return to the root element level when we're done
processing the text of the span.
"""
- self._xTail = etree.SubElement(self._xRoot, TAG_SPAN, attrib={
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={
TAG_STNM: tFmt
})
self._xTail.text = "" # Defaults to None
@@ -1478,20 +1469,20 @@ class XMLParagraph:
if nSpaces == 2:
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
- self._xTail = etree.SubElement(self._xRoot, TAG_SPC)
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPC)
self._xTail.tail = ""
self._nState = X_ROOT_TAIL
self._chrPos += nSpaces - 1
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
- self._xSing = etree.SubElement(self._xTail, TAG_SPC)
+ self._xSing = ET.SubElement(self._xTail, TAG_SPC)
self._xSing.tail = ""
self._nState = X_SPAN_SING
self._chrPos += nSpaces - 1
elif nSpaces > 2:
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
- self._xTail = etree.SubElement(self._xRoot, TAG_SPC, attrib={
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPC, attrib={
TAG_NSPC: str(nSpaces - 1)
})
self._xTail.tail = ""
@@ -1499,7 +1490,7 @@ class XMLParagraph:
self._chrPos += nSpaces - 1
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
- self._xSing = etree.SubElement(self._xTail, TAG_SPC, attrib={
+ self._xSing = ET.SubElement(self._xTail, TAG_SPC, attrib={
TAG_NSPC: str(nSpaces - 1)
})
self._xSing.tail = ""
From a7ffbb0965e31fcd839d590bc9fd8205dbcb69ff Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 20:04:40 +0200
Subject: [PATCH 06/11] Update Odt tests
---
.../coreToOdt_SaveFlat_document.fodt | 63 ++++----
.../reference/coreToOdt_SaveFull_content.xml | 6 +-
.../reference/coreToOdt_SaveFull_manifest.xml | 10 +-
tests/reference/coreToOdt_SaveFull_meta.xml | 8 +-
.../reference/coreToOdt_SaveFull_settings.xml | 2 +-
tests/reference/coreToOdt_SaveFull_styles.xml | 55 +++----
.../guiBuild_Tool_Step1_Lorem_Ipsum.fodt | 82 ++++++-----
tests/test_core/test_core_toodt.py | 138 ++++++++----------
8 files changed, 175 insertions(+), 189 deletions(-)
diff --git a/tests/reference/coreToOdt_SaveFlat_document.fodt b/tests/reference/coreToOdt_SaveFlat_document.fodt
index 6e879a5c..dd64cb33 100644
--- a/tests/reference/coreToOdt_SaveFlat_document.fodt
+++ b/tests/reference/coreToOdt_SaveFlat_document.fodt
@@ -1,81 +1,82 @@
-
+
- 2023-02-12T15:10:11
- novelWriter/2.0.4
+ 2023-05-29T19:49:56
+ novelWriter/2.0.7
Jane Smith
1234
P42DT12H34M56S
Test Project
- 2023-02-12T15:10:11
+ 2023-05-29T19:49:56
Jane Smith
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
- Test Project / Jane Smith / 2
+ Test Project / Jane Smith / 2
+
-
+
diff --git a/tests/reference/coreToOdt_SaveFull_content.xml b/tests/reference/coreToOdt_SaveFull_content.xml
index 4e191f7c..0654b0f3 100644
--- a/tests/reference/coreToOdt_SaveFull_content.xml
+++ b/tests/reference/coreToOdt_SaveFull_content.xml
@@ -1,11 +1,11 @@
-
+
-
+
-
+
diff --git a/tests/reference/coreToOdt_SaveFull_manifest.xml b/tests/reference/coreToOdt_SaveFull_manifest.xml
index f939a7b3..858f5ff8 100644
--- a/tests/reference/coreToOdt_SaveFull_manifest.xml
+++ b/tests/reference/coreToOdt_SaveFull_manifest.xml
@@ -1,8 +1,8 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/tests/reference/coreToOdt_SaveFull_meta.xml b/tests/reference/coreToOdt_SaveFull_meta.xml
index 79930faa..8791d9d7 100644
--- a/tests/reference/coreToOdt_SaveFull_meta.xml
+++ b/tests/reference/coreToOdt_SaveFull_meta.xml
@@ -1,13 +1,13 @@
-
+
- 2023-02-12T15:09:03
- novelWriter/2.0.4
+ 2023-05-29T19:48:33
+ novelWriter/2.0.7
Jane Smith
1234
P42DT12H34M56S
Test Project
- 2023-02-12T15:09:03
+ 2023-05-29T19:48:33
Jane Smith
diff --git a/tests/reference/coreToOdt_SaveFull_settings.xml b/tests/reference/coreToOdt_SaveFull_settings.xml
index 7b5218f2..53e43d1f 100644
--- a/tests/reference/coreToOdt_SaveFull_settings.xml
+++ b/tests/reference/coreToOdt_SaveFull_settings.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/tests/reference/coreToOdt_SaveFull_styles.xml b/tests/reference/coreToOdt_SaveFull_styles.xml
index 5319adf2..6c5f9bd2 100644
--- a/tests/reference/coreToOdt_SaveFull_styles.xml
+++ b/tests/reference/coreToOdt_SaveFull_styles.xml
@@ -1,68 +1,69 @@
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
- Test Project / Jane Smith / 2
+ Test Project / Jane Smith / 2
+
-
+
diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
index d327ee5b..ce4afc35 100644
--- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
@@ -1,106 +1,110 @@
-
+
- 2023-02-12T14:52:03
- novelWriter/2.0.4
+ 2023-05-29T20:00:01
+ novelWriter/2.0.7
lipsum.com
- 38
- P0DT0H31M40S
+ 40
+ P0DT0H31M45S
Lorem Ipsum
- 2023-02-12T14:52:03
+ 2023-05-29T20:00:01
lipsum.com
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- Lorem Ipsum / lipsum.com / 2
+ Lorem Ipsum / lipsum.com / 2
+
-
+
Lorem Ipsum
- By lipsum.com
+
+ By lipsum.com
+
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
Prologue
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
Act One
“Fusce maximus felis libero”
Chapter 1: Chapter One
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 6e3ea446..c85e473f 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -21,12 +21,13 @@ along with this program. If not, see .
import pytest
import zipfile
+import xml.etree.ElementTree as ET
-from lxml import etree
from shutil import copyfile
from tools import ODT_IGNORE, cmpFiles
+from novelwriter.common import xmlIndent
from novelwriter.core.toodt import ToOdt, ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag
from novelwriter.core.project import NWProject
@@ -44,7 +45,7 @@ XML_NS = [
def xmlToText(xElem):
"""Get the text content of an XML element.
"""
- rTxt = etree.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
+ rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for nSpace in XML_NS:
rTxt = rTxt.replace(nSpace, "")
return rTxt
@@ -113,7 +114,7 @@ def testCoreToOdt_TextFormatting(mockGUI):
theDoc = ToOdt(theProject, isFlat=True)
theDoc.initDocument()
- assert xmlToText(theDoc._xText) == ""
+ assert xmlToText(theDoc._xText) == ""
# Paragraph Style
# ===============
@@ -147,7 +148,7 @@ def testCoreToOdt_TextFormatting(mockGUI):
theDoc._addTextPar("Standard", oStyle, "")
assert xmlToText(theDoc._xText) == (
""
- ""
+ ""
""
)
@@ -217,7 +218,7 @@ def testCoreToOdt_TextFormatting(mockGUI):
assert theDoc.getErrors() == []
assert xmlToText(theDoc._xText) == (
""
- "HelloWorld"
+ "HelloWorld"
""
)
@@ -366,7 +367,7 @@ def testCoreToOdt_Convert(mockGUI):
assert theDoc.getErrors() == []
assert xmlToText(theDoc._xText) == (
''
- 'Some text.Next line'
+ 'Some text.Next line'
''
)
@@ -379,7 +380,7 @@ def testCoreToOdt_Convert(mockGUI):
assert theDoc.getErrors() == []
assert xmlToText(theDoc._xText) == (
''
- 'Item 1Item 2'
+ 'Item 1Item 2'
''
)
@@ -393,7 +394,7 @@ def testCoreToOdt_Convert(mockGUI):
assert xmlToText(theDoc._xText) == (
''
'Some '
- 'boldtext'
+ 'boldtext'
''
)
@@ -413,8 +414,8 @@ def testCoreToOdt_Convert(mockGUI):
''
'Scene'
'Hello World'
- 'Hello World'
- 'Hello World'
+ 'Hello World'
+ 'Hello World'
''
)
@@ -474,9 +475,9 @@ def testCoreToOdt_Convert(mockGUI):
assert theDoc.getErrors() == []
assert xmlToText(theDoc._xText) == (
''
- ''
+ ''
'Text'
- ''
+ ''
'Text'
''
)
@@ -540,7 +541,7 @@ def testCoreToOdt_Convert(mockGUI):
''
'Scene'
'Regular paragraph'
- 'withbreak'
+ 'withbreak'
'Left Align'
''
)
@@ -592,7 +593,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
assert (
''
- ''
+ ''
''
) in xmlToText(theDoc._xAuto)
assert xmlToText(theDoc._xText) == (
@@ -613,7 +614,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
assert (
''
- ''
+ ''
''
) in xmlToText(theDoc._xAuto)
assert xmlToText(theDoc._xText) == (
@@ -723,15 +724,10 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
stylOut = tstPaths.outDir / "coreToOdt_SaveFull" / "styles.xml"
def prettifyXml(inFile, outFile):
- with open(outFile, mode="wb") as fileStream:
- fileStream.write(
- etree.tostring(
- etree.parse(str(inFile)),
- pretty_print=True,
- encoding="utf-8",
- xml_declaration=True
- )
- )
+ with open(outFile, mode="wb") as fStream:
+ xml = ET.parse(inFile)
+ xmlIndent(xml, space=" ")
+ xml.write(fStream, encoding="utf-8", xml_declaration=True)
prettifyXml(maniOut, maniFile)
prettifyXml(settOut, settFile)
@@ -949,20 +945,16 @@ def testCoreToOdt_ODTParagraphStyle():
# Pack XML
# ========
- xStyle = etree.Element("test", nsmap={
- "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
- "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
- "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
- })
+ xStyle = ET.Element("test")
parStyle.packXML(xStyle, "test")
assert xmlToText(xStyle) == (
''
''
''
+ 'fo:margin-left="0.000cm" fo:margin-right="0.000cm" fo:line-height="1.15" />'
''
+ 'fo:font-size="12pt" fo:color="#000000" loext:opacity="1.00" />'
''
''
)
@@ -1059,15 +1051,12 @@ def testCoreToOdt_ODTTextStyle():
txtStyle.setFontStyle("italic")
txtStyle.setStrikeStyle("solid")
txtStyle.setStrikeType("single")
- xStyle = etree.Element("test", nsmap={
- "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
- "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
- })
+ xStyle = ET.Element("test")
txtStyle.packXML(xStyle, "test")
assert xmlToText(xStyle) == (
''
+ 'style:text-line-through-type="single" />'
)
# END Test testCoreToOdt_ODTTextStyle
@@ -1077,20 +1066,11 @@ def testCoreToOdt_ODTTextStyle():
def testCoreToOdt_XMLParagraph():
"""Test XML encoding of paragraph.
"""
- nsMap = {
- "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
- "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
- "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
- "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
- "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
- "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
- }
-
# Stage 1 : Text
# ==============
- xRoot = etree.Element("root", nsmap=nsMap)
- xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"])
+ xRoot = ET.Element("root")
+ xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
xmlPar = XMLParagraph(xElem)
# Plain Text
@@ -1126,15 +1106,15 @@ def testCoreToOdt_XMLParagraph():
# Stage 2 : Line Breaks
# =====================
- xRoot = etree.Element("root", nsmap=nsMap)
- xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"])
+ xRoot = ET.Element("root")
+ xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
xmlPar = XMLParagraph(xElem)
# Plain Text w/Line Break
xmlPar.appendText("Hello\nWorld\n!!")
assert xmlToText(xRoot) == (
''
- 'HelloWorld!!'
+ 'HelloWorld!!'
''
)
@@ -1142,8 +1122,8 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendSpan("spanned\ntext", "T1")
assert xmlToText(xRoot) == (
''
- 'HelloWorld!!'
- 'spannedtext'
+ 'HelloWorld!!'
+ 'spannedtext'
''
)
@@ -1151,9 +1131,9 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendText("more\ntext")
assert xmlToText(xRoot) == (
''
- 'HelloWorld!!'
- 'spannedtext'
- 'moretext'
+ 'HelloWorld!!'
+ 'spannedtext'
+ 'moretext'
''
)
@@ -1162,15 +1142,15 @@ def testCoreToOdt_XMLParagraph():
# Stage 3 : Tabs
# ==============
- xRoot = etree.Element("root", nsmap=nsMap)
- xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"])
+ xRoot = ET.Element("root")
+ xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
xmlPar = XMLParagraph(xElem)
# Plain Text w/Line Break
xmlPar.appendText("Hello\tWorld\t!!")
assert xmlToText(xRoot) == (
''
- 'HelloWorld!!'
+ 'HelloWorld!!'
''
)
@@ -1178,8 +1158,8 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendSpan("spanned\ttext", "T1")
assert xmlToText(xRoot) == (
''
- 'HelloWorld!!'
- 'spannedtext'
+ 'HelloWorld!!'
+ 'spannedtext'
''
)
@@ -1187,9 +1167,9 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendText("more\ttext")
assert xmlToText(xRoot) == (
''
- 'HelloWorld!!'
- 'spannedtext'
- 'moretext'
+ 'HelloWorld!!'
+ 'spannedtext'
+ 'moretext'
''
)
@@ -1198,15 +1178,15 @@ def testCoreToOdt_XMLParagraph():
# Stage 4 : Spaces
# ================
- xRoot = etree.Element("root", nsmap=nsMap)
- xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"])
+ xRoot = ET.Element("root")
+ xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
xmlPar = XMLParagraph(xElem)
# Plain Text w/Spaces
xmlPar.appendText("Hello World !!")
assert xmlToText(xRoot) == (
''
- 'Hello World !!'
+ 'Hello World !!'
''
)
@@ -1214,8 +1194,8 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendSpan("spanned text", "T1")
assert xmlToText(xRoot) == (
''
- 'Hello World !!'
- 'spanned text'
+ 'Hello World !!'
+ 'spanned text'
''
)
@@ -1223,9 +1203,9 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendText("more text")
assert xmlToText(xRoot) == (
''
- 'Hello World !!'
- 'spanned text'
- 'more text'
+ 'Hello World !!'
+ 'spanned text'
+ 'more text'
''
)
@@ -1234,15 +1214,15 @@ def testCoreToOdt_XMLParagraph():
# Stage 5 : Lots of Spaces
# ========================
- xRoot = etree.Element("root", nsmap=nsMap)
- xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"])
+ xRoot = ET.Element("root")
+ xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
xmlPar = XMLParagraph(xElem)
# Plain Text w/Many Spaces
xmlPar.appendText(" \t A \n B ")
assert xmlToText(xRoot) == (
''
- ' A B '
+ ' A B '
''
)
@@ -1250,9 +1230,9 @@ def testCoreToOdt_XMLParagraph():
xmlPar.appendSpan(" C \t D \n E ", "T1")
assert xmlToText(xRoot) == (
''
- ' A B '
- ' C D '
- ' E '
+ ' A B '
+ ' C D '
+ ' E '
''
)
@@ -1261,8 +1241,8 @@ def testCoreToOdt_XMLParagraph():
# Check Error
# ===========
- xRoot = etree.Element("root", nsmap=nsMap)
- xElem = etree.SubElement(xRoot, "{%s}p" % nsMap["text"])
+ xRoot = ET.Element("root")
+ xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
xmlPar = XMLParagraph(xElem)
xmlPar.appendText("A")
From 2a82fd628c0efab39fc8ea2027567cf397bc4884 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 21:34:48 +0200
Subject: [PATCH 07/11] Fix logging bug in init
---
novelwriter/__init__.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 8597a55c..ccc9a333 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -155,13 +155,14 @@ def main(sysArgs=None):
elif inOpt == "--testmode":
testMode = True
- # Set Logging
- cHandle = logging.StreamHandler()
- cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
-
+ # Setup Logging
pkgLogger = logging.getLogger(__package__)
- pkgLogger.addHandler(cHandle)
pkgLogger.setLevel(logLevel)
+ if len(pkgLogger.handlers) == 0:
+ # Make sure we only create one logger (mostly an issue with tests)
+ cHandle = logging.StreamHandler()
+ cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
+ pkgLogger.addHandler(cHandle)
logger.info("Starting novelWriter %s (%s) %s", __version__, __hexversion__, __date__)
From a40275c431009bd5b55afe11695d298617ecc967 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 21:35:16 +0200
Subject: [PATCH 08/11] Make the xmlIndent function the permanent solution
---
novelwriter/common.py | 85 +++++++++++++++++++++----------------------
1 file changed, 41 insertions(+), 44 deletions(-)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index f4161a31..5813ec1e 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -23,6 +23,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from __future__ import annotations
+
import json
import uuid
import hashlib
@@ -436,6 +438,45 @@ def jsonEncode(data, n=0, nmax=0):
return "".join(buffer)
+def xmlIndent(tree: ET.Element | ET.ElementTree):
+ """A modified version of the XML indent function in the standard
+ library. It behaves more closely to how the one from lxml does.
+ """
+ if isinstance(tree, ET.ElementTree):
+ tree = tree.getroot()
+
+ indentations = ["\n"]
+
+ def indentChildren(elem, level):
+ chLevel = level + 1
+ try:
+ chIndent = indentations[chLevel]
+ except IndexError:
+ chIndent = indentations[level] + " "
+ indentations.append(chIndent)
+
+ if elem.text is None:
+ elem.text = chIndent
+
+ last = None
+ for child in elem:
+ if len(child):
+ indentChildren(child, chLevel)
+ if child.tail is None:
+ child.tail = chIndent
+ last = child
+
+ # Dedent the last child
+ if last is not None:
+ last.tail = indentations[level]
+
+ if len(tree):
+ indentChildren(tree, 0)
+ tree.tail = "\n"
+
+ return
+
+
# =============================================================================================== #
# File and File System Functions
# =============================================================================================== #
@@ -563,47 +604,3 @@ class NWConfigParser(ConfigParser):
return result
# END Class NWConfigParser
-
-
-# =============================================================================================== #
-# Third Party Code
-# =============================================================================================== #
-
-def xmlIndent(tree, space=" ", level=0): # pragma: no cover
- """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)
From 1d90d6d3859e61d94f699844ef1f36d3bcff0598 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 21:47:13 +0200
Subject: [PATCH 09/11] Update XML classes and tests
---
novelwriter/core/projectxml.py | 6 +-
novelwriter/core/toodt.py | 33 ++++---
sample/nwProject.nwx | 8 +-
.../guiBuild_Tool_Step1_Lorem_Ipsum.fodt | 11 +--
.../guiBuild_Tool_Step2_Lorem_Ipsum.fodt | 89 ++++++++++---------
.../guiBuild_Tool_Step3_Lorem_Ipsum.fodt | 89 ++++++++++---------
tests/test_core/test_core_toodt.py | 55 ++++++------
7 files changed, 143 insertions(+), 148 deletions(-)
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index a103774b..b39356a2 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -25,7 +25,6 @@ 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
@@ -561,10 +560,7 @@ class ProjectXMLWriter:
backFile = saveFile.with_suffix(".bak")
try:
xml = ET.ElementTree(xRoot)
- if sys.hexversion < 0x030900f0:
- xmlIndent(xml, space=" ")
- else:
- ET.indent(xml, space=" ")
+ xmlIndent(xml)
xml.write(tempFile, encoding="utf-8", xml_declaration=True)
except Exception as exc:
self._error = exc
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index dd980d23..1725a4c7 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -92,20 +92,20 @@ class ToOdt(Tokenizer):
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
- self._dFlat = None # FODT file XML root
- self._dCont = None # ODT content.xml root
- self._dMeta = None # ODT meta.xml root
- self._dStyl = None # ODT styles.xml root
+ self._dFlat = ET.Element("") # FODT file XML root
+ self._dCont = ET.Element("") # ODT content.xml root
+ self._dMeta = ET.Element("") # ODT meta.xml root
+ self._dStyl = ET.Element("") # ODT styles.xml root
- self._xMeta = None # Office meta root
- self._xFont = None # Office font face declaration
- self._xFnt2 = None # Office font face declaration, secondary
- self._xStyl = None # Office styles root
- self._xAuto = None # Office auto-styles root
- self._xAut2 = None # Office auto-styles root, secondary
- self._xMast = None # Office master-styles root
- self._xBody = None # Office body root
- self._xText = None # Office text root
+ self._xMeta = ET.Element("") # Office meta root
+ self._xFont = ET.Element("") # Office font face declaration
+ self._xFnt2 = ET.Element("") # Office font face declaration, secondary
+ self._xStyl = ET.Element("") # Office styles root
+ self._xAuto = ET.Element("") # Office auto-styles root
+ self._xAut2 = ET.Element("") # Office auto-styles root, secondary
+ self._xMast = ET.Element("") # Office master-styles root
+ self._xBody = ET.Element("") # Office body root
+ self._xText = ET.Element("") # Office text root
self._mainPara = {} # User-accessible paragraph styles
self._autoPara = {} # Auto-generated paragraph styles
@@ -502,7 +502,7 @@ class ToOdt(Tokenizer):
"""
with open(savePath, mode="wb") as outFile:
xml = ET.ElementTree(self._dFlat)
- xmlIndent(xml, space=" ")
+ xmlIndent(xml)
xml.write(outFile, encoding="utf-8", xml_declaration=True)
return
@@ -529,7 +529,6 @@ class ToOdt(Tokenizer):
def putInZip(name, xObj, zipObj):
with zipObj.open(name, mode="w") as fObj:
xml = ET.ElementTree(xObj)
- xmlIndent(xml, space=" ")
xml.write(fObj, encoding="utf-8", xml_declaration=True)
with ZipFile(savePath, mode="w") as outZip:
@@ -1337,8 +1336,8 @@ class XMLParagraph:
def __init__(self, xRoot):
self._xRoot = xRoot
- self._xTail = None
- self._xSing = None
+ self._xTail = ET.Element("")
+ self._xSing = ET.Element("")
self._nState = X_ROOT_TEXT
self._chrPos = 0
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 96e22a4a..afb0157a 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
@@ -65,7 +65,7 @@
Chapter One
-
-
+
Making a Scene
-
@@ -153,4 +153,4 @@
Delete Me!
-
\ No newline at end of file
+
diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
index ce4afc35..de682d59 100644
--- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
@@ -1,13 +1,13 @@
- 2023-05-29T20:00:01
+ 2023-05-29T20:51:32
novelWriter/2.0.7
lipsum.com
40
P0DT0H31M45S
Lorem Ipsum
- 2023-05-29T20:00:01
+ 2023-05-29T20:51:32
lipsum.com
@@ -95,16 +95,13 @@
Lorem Ipsum
-
- By lipsum.com
-
+ By lipsum.com
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
Prologue
-
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
Act One
“Fusce maximus felis libero”
Chapter 1: Chapter One
diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt
index d179c99d..ff23fc4a 100644
--- a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt
@@ -1,105 +1,106 @@
-
+
- 2023-02-12T15:00:22
- novelWriter/2.0.4
+ 2023-05-29T20:57:00
+ novelWriter/2.0.7
lipsum.com
- 38
- P0DT0H31M40S
+ 40
+ P0DT0H31M45S
Lorem Ipsum
- 2023-02-12T15:00:22
+ 2023-05-29T20:57:00
lipsum.com
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- Lorem Ipsum / lipsum.com / 2
+ Lorem Ipsum / lipsum.com / 2
+
-
+
@@ -147,9 +148,9 @@
Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus.
Why do we use it?
Comment: Exctracted from the lipsum.com website.
- It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
- The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.
- Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
+ It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
+ The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.
+ Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
Chapter Two: Chapter Two
Point of View: Bod
Plot: Main
diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt
index ca391d27..dfa9262c 100644
--- a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt
@@ -1,105 +1,106 @@
-
+
- 2023-02-12T15:01:25
- novelWriter/2.0.4
+ 2023-05-29T21:06:32
+ novelWriter/2.0.7
lipsum.com
- 38
- P0DT0H31M40S
+ 40
+ P0DT0H31M45S
Lorem Ipsum
- 2023-02-12T15:01:25
+ 2023-05-29T21:06:32
lipsum.com
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- Lorem Ipsum / lipsum.com / 2
+ Lorem Ipsum / lipsum.com / 2
+
-
+
@@ -147,9 +148,9 @@
Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus.
Why do we use it?
Comment: Exctracted from the lipsum.com website.
- It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
- The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.
- Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
+ It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
+ The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.
+ Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
Chapter Two: Chapter Two
Point of View: Bod
Plot: Main
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index c85e473f..55c6d785 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -64,21 +64,21 @@ def testCoreToOdt_Init(mockGUI):
theDoc.initDocument()
# Document XML
- assert theDoc._dFlat is not None
- assert theDoc._dCont is None
- assert theDoc._dMeta is None
- assert theDoc._dStyl is None
+ assert theDoc._dFlat.tag == _mkTag("office", "document")
+ assert theDoc._dCont.tag == ""
+ assert theDoc._dMeta.tag == ""
+ assert theDoc._dStyl.tag == ""
# Content XML
- assert theDoc._xMeta is not None
- assert theDoc._xFont is not None
- assert theDoc._xFnt2 is None
- assert theDoc._xStyl is not None
- assert theDoc._xAuto is not None
- assert theDoc._xAut2 is None
- assert theDoc._xMast is not None
- assert theDoc._xBody is not None
- assert theDoc._xText is not None
+ assert theDoc._xMeta.tag == _mkTag("office", "meta")
+ assert theDoc._xFont.tag == _mkTag("office", "font-face-decls")
+ assert theDoc._xFnt2.tag == ""
+ assert theDoc._xStyl.tag == _mkTag("office", "styles")
+ assert theDoc._xAuto.tag == _mkTag("office", "automatic-styles")
+ assert theDoc._xAut2.tag == ""
+ assert theDoc._xMast.tag == _mkTag("office", "master-styles")
+ assert theDoc._xBody.tag == _mkTag("office", "body")
+ assert theDoc._xText.tag == _mkTag("office", "text")
# ODT Doc
# =======
@@ -87,21 +87,22 @@ def testCoreToOdt_Init(mockGUI):
theDoc.initDocument()
# Document XML
- assert theDoc._dFlat is None
- assert theDoc._dCont is not None
- assert theDoc._dMeta is not None
- assert theDoc._dStyl is not None
+ assert theDoc._dFlat.tag == ""
+ assert theDoc._dCont.tag == _mkTag("office", "document-content")
+ assert theDoc._dMeta.tag == _mkTag("office", "document-meta")
+ assert theDoc._dStyl.tag == _mkTag("office", "document-styles")
# Content XML
- assert theDoc._xMeta is not None
- assert theDoc._xFont is not None
- assert theDoc._xFnt2 is not None
- assert theDoc._xStyl is not None
- assert theDoc._xAuto is not None
- assert theDoc._xAut2 is not None
- assert theDoc._xMast is not None
- assert theDoc._xBody is not None
- assert theDoc._xText is not None
+ assert theDoc._xMeta.tag == _mkTag("office", "meta")
+ assert theDoc._xFont.tag == _mkTag("office", "font-face-decls")
+ assert theDoc._xFnt2.tag == _mkTag("office", "font-face-decls")
+ assert theDoc._xStyl.tag == _mkTag("office", "styles")
+ assert theDoc._xAuto.tag == _mkTag("office", "automatic-styles")
+ assert theDoc._xAut2.tag == _mkTag("office", "automatic-styles")
+ assert theDoc._xMast.tag == _mkTag("office", "master-styles")
+ assert theDoc._xBody.tag == _mkTag("office", "body")
+ assert theDoc._xText.tag == _mkTag("office", "text")
+
# END Test testCoreToOdt_Init
@@ -726,7 +727,7 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
def prettifyXml(inFile, outFile):
with open(outFile, mode="wb") as fStream:
xml = ET.parse(inFile)
- xmlIndent(xml, space=" ")
+ xmlIndent(xml)
xml.write(fStream, encoding="utf-8", xml_declaration=True)
prettifyXml(maniOut, maniFile)
From af923d60502252cde9d37b9d287a5ff8ea86400f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 21:52:43 +0200
Subject: [PATCH 10/11] Remove references and checks for lxml
---
CREDITS.md | 1 -
docs/source/int_source.rst | 5 +----
novelwriter/__init__.py | 6 ------
novelwriter/assets/text/credits_en.htm | 1 -
novelwriter/core/toodt.py | 4 +---
novelwriter/error.py | 7 -------
setup.cfg | 1 -
setup/debian/control | 4 ++--
setup/windows_install.bat | 2 +-
setup/windows_uninstall.bat | 2 +-
tests/test_base/test_base_init.py | 3 ---
11 files changed, 6 insertions(+), 30 deletions(-)
diff --git a/CREDITS.md b/CREDITS.md
index 489495dd..d792ce6f 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -40,7 +40,6 @@ The following libraries are dependencies of novelWriter:
* [Qt5](https://www.qt.io) by Qt Company
* [PyQt5](https://www.riverbankcomputing.com/software/pyqt) by Riverbank Computing
-* [lxml](https://lxml.de) by Martijn Faassen
* [Enchant](https://abiword.github.io/enchant) by Dom Lachowicz
* [PyEnchant](https://pyenchant.github.io/pyenchant) by Dimitri Merejkowsky
diff --git a/docs/source/int_source.rst b/docs/source/int_source.rst
index 3ae3bfaf..3630b537 100644
--- a/docs/source/int_source.rst
+++ b/docs/source/int_source.rst
@@ -46,13 +46,10 @@ format of the main project file. Everything else is handled with standard Python
The following Python packages are needed to run novelWriter:
* ``PyQt5`` – needed for connecting with the Qt5 libraries.
-* ``lxml`` – needed for full XML support.
* ``PyEnchant`` – needed for spell checking (optional).
PyQt/Qt should be at least 5.10, but ideally 5.13 or higher for all features to work. For instance,
-searching using regular expressions with full Unicode support requires 5.13. There is no known
-minimum version requirement for package ``lxml``, but the code was originally written with 4.2,
-which is therefore set as the minimum. It may work on lower versions. You have to test it.
+searching using regular expressions with full Unicode support requires 5.13.
If you want spell checking, you must install the ``PyEnchant`` package. The spell check library
must be at least 3.0 to work with Windows. On Linux, 2.0 also works fine.
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index ccc9a333..c85d949b 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -185,12 +185,6 @@ def main(sysArgs=None):
)
errorCode |= 0x10
- try:
- import lxml # noqa: F401
- except ImportError:
- errorData.append("Python module 'lxml' is missing")
- errorCode |= 0x20
-
if errorData:
errApp = QApplication([])
errDlg = QErrorMessage()
diff --git a/novelwriter/assets/text/credits_en.htm b/novelwriter/assets/text/credits_en.htm
index 80790378..525ffca9 100644
--- a/novelwriter/assets/text/credits_en.htm
+++ b/novelwriter/assets/text/credits_en.htm
@@ -47,7 +47,6 @@ more contributions are listed on the project's Qt5 by Qt Company
PyQt5 by Riverbank Computing
- lxml by Martijn Faassen
Enchant by Dom Lachowicz
PyEnchant by Dimitri Merejkowsky
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 1725a4c7..8a351a6a 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -597,7 +597,7 @@ class ToOdt(Tokenizer):
xElem = ET.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
# It's important to set the initial text field to empty, otherwise
- # lxml will add a line break if the first subelement is a span.
+ # xmlIndent will add a line break if the first subelement is a span.
xElem.text = ""
if not theText:
@@ -1315,8 +1315,6 @@ class XMLParagraph:
"""This is a helper class to manage the text content of a single
XML element using mixed content tags.
- See: https://lxml.de/tutorial.html#the-element-class
-
Rules:
* The root tag can only have text set, never tail.
* Any span must be under root, and the text in the span is set in
diff --git a/novelwriter/error.py b/novelwriter/error.py
index 74f45318..441c138f 100644
--- a/novelwriter/error.py
+++ b/novelwriter/error.py
@@ -128,12 +128,6 @@ class NWErrorMessage(QDialog):
except Exception:
kernelVersion = "Unknown"
- try:
- import lxml
- lxmlVersion = lxml.__version__ # type: ignore
- except Exception:
- lxmlVersion = "Unknown"
-
try:
import enchant
enchantVersion = enchant.__version__
@@ -148,7 +142,6 @@ class NWErrorMessage(QDialog):
f"Host OS: {sys.platform} ({kernelVersion})\n"
f"Python: {sys.version.split()[0]} ({sys.hexversion:#x})\n"
f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n"
- f"lxml: {lxmlVersion}\n"
f"enchant: {enchantVersion}\n\n"
f"{exType.__name__}:\n{str(exValue)}\n\n"
f"Traceback:\n{exTrace}\n"
diff --git a/setup.cfg b/setup.cfg
index 6111f0d1..b9572076 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -35,7 +35,6 @@ include_package_data = True
packages = find_namespace:
install_requires =
pyqt5>=5.10
- lxml>=4.2.0
pyenchant>=3.0.0
[options.packages.find]
diff --git a/setup/debian/control b/setup/debian/control
index bbcb49d7..0fb2616b 100644
--- a/setup/debian/control
+++ b/setup/debian/control
@@ -2,14 +2,14 @@ Source: novelwriter
Maintainer: Veronica Berglyd Olsen
Section: text
Priority: optional
-Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.7), python3-pyqt5 (>= 5.10), python3-lxml (>= 4.0), python3-enchant (>= 2.0)
+Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.7), python3-pyqt5 (>= 5.10), python3-enchant (>= 2.0)
Standards-Version: 4.5.1
Homepage: https://novelwriter.io
X-Python3-Version: >= 3.7
Package: novelwriter
Architecture: all
-Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.7), python3-pyqt5 (>= 5.10), python3-lxml (>= 4.0), python3-enchant (>= 2.0)
+Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.7), python3-pyqt5 (>= 5.10), python3-enchant (>= 2.0)
Description: A markdown-like text editor for planning and writing novels
novelWriter is a plain text editor designed for writing novels assembled from
many smaller text documents. It uses a minimal formatting syntax inspired by
diff --git a/setup/windows_install.bat b/setup/windows_install.bat
index 474c8861..4b2c80d6 100644
--- a/setup/windows_install.bat
+++ b/setup/windows_install.bat
@@ -19,7 +19,7 @@ if exist setup.py (
)
echo.
-:: Install the PyQt5, lxml and pyenchant dependencies
+:: Install the PyQt5 and pyenchant dependencies
pip install --user pywin32 -r requirements.txt
:: Create the desktop and start menu icons
diff --git a/setup/windows_uninstall.bat b/setup/windows_uninstall.bat
index 7522f4bd..18523176 100644
--- a/setup/windows_uninstall.bat
+++ b/setup/windows_uninstall.bat
@@ -29,7 +29,7 @@ goto afterUninst
:: Remove the desktop and start menu icons
python setup.py win-uninstall
-:: Remove the PyQt5, lxml and pyenchant dependencies
+:: Remove the PyQt5 and pyenchant dependencies
pip uninstall --yes -r requirements.txt
:afterUninst
diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py
index 71056fb7..a932dcfa 100644
--- a/tests/test_base/test_base_init.py
+++ b/tests/test_base/test_base_init.py
@@ -154,7 +154,6 @@ def testBaseInit_Imports(caplog, monkeypatch, fncPath):
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.__init__", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.resize", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.showMessage", lambda *a: None)
- monkeypatch.setitem(sys.modules, "lxml", None)
monkeypatch.setattr("sys.hexversion", 0x0)
monkeypatch.setattr("novelwriter.CONFIG.verQtValue", 0x050000)
monkeypatch.setattr("novelwriter.CONFIG.verPyQtValue", 0x050000)
@@ -167,11 +166,9 @@ def testBaseInit_Imports(caplog, monkeypatch, fncPath):
assert ex.value.code & 4 == 4 # Python version not satisfied
assert ex.value.code & 8 == 8 # Qt version not satisfied
assert ex.value.code & 16 == 16 # PyQt version not satisfied
- assert ex.value.code & 32 == 32 # lxml package missing
assert "At least Python" in caplog.messages[0]
assert "At least Qt5" in caplog.messages[1]
assert "At least PyQt5" in caplog.messages[2]
- assert "lxml" in caplog.messages[3]
# END Test testBaseInit_Imports
From ebc3b1f74538e0d887c4785272148eaa39d4b0c8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 29 May 2023 22:12:55 +0200
Subject: [PATCH 11/11] Fix redundant line wrapping in ODT header style
---
novelwriter/core/toodt.py | 1 +
tests/reference/coreToOdt_SaveFlat_document.fodt | 7 +++----
tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt | 7 +++----
tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt | 7 +++----
tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt | 7 +++----
5 files changed, 13 insertions(+), 16 deletions(-)
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 8a351a6a..b707f46c 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -992,6 +992,7 @@ class ToOdt(Tokenizer):
_mkTag("text", "select-page"): "current"
})
xTail.text = "2"
+ xTail.tail = "" # Prevent line break in indented XML
# First Page Header
xHead = ET.SubElement(xPage, _mkTag("style", "header-first"))
diff --git a/tests/reference/coreToOdt_SaveFlat_document.fodt b/tests/reference/coreToOdt_SaveFlat_document.fodt
index dd64cb33..0c2b658e 100644
--- a/tests/reference/coreToOdt_SaveFlat_document.fodt
+++ b/tests/reference/coreToOdt_SaveFlat_document.fodt
@@ -1,13 +1,13 @@
- 2023-05-29T19:49:56
+ 2023-05-29T22:10:15
novelWriter/2.0.7
Jane Smith
1234
P42DT12H34M56S
Test Project
- 2023-05-29T19:49:56
+ 2023-05-29T22:10:15
Jane Smith
@@ -72,8 +72,7 @@
- Test Project / Jane Smith / 2
-
+ Test Project / Jane Smith / 2
diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
index de682d59..4bd71182 100644
--- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.fodt
@@ -1,13 +1,13 @@
- 2023-05-29T20:51:32
+ 2023-05-29T22:10:29
novelWriter/2.0.7
lipsum.com
40
P0DT0H31M45S
Lorem Ipsum
- 2023-05-29T20:51:32
+ 2023-05-29T22:10:29
lipsum.com
@@ -84,8 +84,7 @@
- Lorem Ipsum / lipsum.com / 2
-
+ Lorem Ipsum / lipsum.com / 2
diff --git a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt
index ff23fc4a..cf329eb6 100644
--- a/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step2_Lorem_Ipsum.fodt
@@ -1,13 +1,13 @@
- 2023-05-29T20:57:00
+ 2023-05-29T22:11:22
novelWriter/2.0.7
lipsum.com
40
P0DT0H31M45S
Lorem Ipsum
- 2023-05-29T20:57:00
+ 2023-05-29T22:11:22
lipsum.com
@@ -96,8 +96,7 @@
- Lorem Ipsum / lipsum.com / 2
-
+ Lorem Ipsum / lipsum.com / 2
diff --git a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt
index dfa9262c..d5891721 100644
--- a/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt
+++ b/tests/reference/guiBuild_Tool_Step3_Lorem_Ipsum.fodt
@@ -1,13 +1,13 @@
- 2023-05-29T21:06:32
+ 2023-05-29T22:11:58
novelWriter/2.0.7
lipsum.com
40
P0DT0H31M45S
Lorem Ipsum
- 2023-05-29T21:06:32
+ 2023-05-29T22:11:58
lipsum.com
@@ -96,8 +96,7 @@
- Lorem Ipsum / lipsum.com / 2
-
+ Lorem Ipsum / lipsum.com / 2