novelWriter 2.2.1__py3-none-any.whl → 2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/RECORD +116 -101
- novelWriter-2.3.dist-info/entry_points.txt +2 -0
- novelwriter/__init__.py +4 -4
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/project_nl_NL.json +11 -0
- novelwriter/assets/i18n/project_pt_BR.json +11 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
- novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
- novelwriter/assets/images/novelwriter-text-light.svg +4 -0
- novelwriter/assets/images/welcome-dark.jpg +0 -0
- novelwriter/assets/images/welcome-light.jpg +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tango.conf +23 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +4 -2
- novelwriter/assets/themes/cyberpunk_night.conf +29 -0
- novelwriter/assets/themes/default_dark.conf +2 -2
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +48 -37
- novelwriter/config.py +36 -41
- novelwriter/constants.py +38 -16
- novelwriter/core/buildsettings.py +7 -7
- novelwriter/core/coretools.py +196 -156
- novelwriter/core/docbuild.py +6 -3
- novelwriter/core/document.py +6 -6
- novelwriter/core/index.py +89 -56
- novelwriter/core/item.py +21 -3
- novelwriter/core/options.py +8 -7
- novelwriter/core/project.py +70 -44
- novelwriter/core/projectdata.py +1 -14
- novelwriter/core/projectxml.py +13 -41
- novelwriter/core/sessions.py +2 -1
- novelwriter/core/spellcheck.py +2 -1
- novelwriter/core/status.py +2 -1
- novelwriter/core/storage.py +182 -140
- novelwriter/core/tohtml.py +4 -2
- novelwriter/core/tokenizer.py +109 -82
- novelwriter/core/toodt.py +40 -30
- novelwriter/core/tree.py +3 -2
- novelwriter/dialogs/about.py +70 -160
- novelwriter/dialogs/docmerge.py +6 -5
- novelwriter/dialogs/docsplit.py +6 -6
- novelwriter/dialogs/editlabel.py +1 -1
- novelwriter/dialogs/preferences.py +553 -703
- novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
- novelwriter/dialogs/quotes.py +27 -23
- novelwriter/dialogs/wordlist.py +96 -40
- novelwriter/enum.py +20 -18
- novelwriter/error.py +1 -1
- novelwriter/extensions/circularprogress.py +11 -11
- novelwriter/extensions/configlayout.py +185 -134
- novelwriter/extensions/modified.py +81 -0
- novelwriter/extensions/novelselector.py +26 -12
- novelwriter/extensions/pagedsidebar.py +14 -16
- novelwriter/extensions/simpleprogress.py +5 -5
- novelwriter/extensions/statusled.py +8 -8
- novelwriter/extensions/switch.py +31 -63
- novelwriter/extensions/switchbox.py +1 -1
- novelwriter/extensions/versioninfo.py +153 -0
- novelwriter/gui/doceditor.py +178 -150
- novelwriter/gui/dochighlight.py +63 -92
- novelwriter/gui/docviewer.py +49 -51
- novelwriter/gui/docviewerpanel.py +72 -24
- novelwriter/gui/itemdetails.py +7 -7
- novelwriter/gui/mainmenu.py +14 -19
- novelwriter/gui/noveltree.py +9 -8
- novelwriter/gui/outline.py +98 -75
- novelwriter/gui/projtree.py +241 -106
- novelwriter/gui/sidebar.py +3 -4
- novelwriter/gui/statusbar.py +3 -4
- novelwriter/gui/theme.py +69 -70
- novelwriter/guimain.py +51 -156
- novelwriter/shared.py +15 -1
- novelwriter/tools/dictionaries.py +5 -6
- novelwriter/tools/manuscript.py +6 -6
- novelwriter/tools/manussettings.py +192 -221
- novelwriter/tools/noveldetails.py +525 -0
- novelwriter/tools/welcome.py +819 -0
- novelwriter/tools/writingstats.py +9 -9
- novelWriter-2.2.1.dist-info/entry_points.txt +0 -5
- novelwriter/assets/images/wizard-back.jpg +0 -0
- novelwriter/assets/text/gplv3_en.htm +0 -641
- novelwriter/assets/text/release_notes.htm +0 -60
- novelwriter/dialogs/projdetails.py +0 -518
- novelwriter/dialogs/projload.py +0 -294
- novelwriter/dialogs/updates.py +0 -172
- novelwriter/extensions/pageddialog.py +0 -130
- novelwriter/tools/projwizard.py +0 -478
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/WHEEL +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
novelwriter/core/projectxml.py
CHANGED
@@ -38,7 +38,6 @@ from novelwriter.common import (
|
|
38
38
|
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
39
39
|
hexToInt, simplified, xmlIndent, yesNo
|
40
40
|
)
|
41
|
-
from novelwriter.constants import nwFiles
|
42
41
|
|
43
42
|
if TYPE_CHECKING: # pragma: no cover
|
44
43
|
from novelwriter.core.status import NWStatus
|
@@ -47,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
47
46
|
logger = logging.getLogger(__name__)
|
48
47
|
|
49
48
|
FILE_VERSION = "1.5" # The current project file format version
|
50
|
-
FILE_REVISION = "
|
49
|
+
FILE_REVISION = "2" # The current project file format revision
|
51
50
|
HEX_VERSION = 0x0105
|
52
51
|
|
53
52
|
NUM_VERSION = {
|
@@ -65,12 +64,11 @@ class XMLReadState(Enum):
|
|
65
64
|
|
66
65
|
NO_ACTION = 0
|
67
66
|
NO_ERROR = 1
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
WAS_LEGACY = 7
|
67
|
+
CANNOT_PARSE = 2
|
68
|
+
NOT_NWX_FILE = 3
|
69
|
+
UNKNOWN_VERSION = 4
|
70
|
+
PARSED_OK = 5
|
71
|
+
WAS_LEGACY = 6
|
74
72
|
|
75
73
|
# END Class XMLReadState
|
76
74
|
|
@@ -107,7 +105,9 @@ class ProjectXMLReader:
|
|
107
105
|
the project or the content into their respective section nodes
|
108
106
|
as attributes. The id attribute was also added to the project.
|
109
107
|
|
110
|
-
Rev 1: Drops the titleFormat
|
108
|
+
Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1.
|
109
|
+
Rev 2: Drops the title node from project and adds the TEMPLATE
|
110
|
+
class for items. 2.3 Beta 1.
|
111
111
|
"""
|
112
112
|
|
113
113
|
def __init__(self, path: str | Path) -> None:
|
@@ -172,23 +172,9 @@ class ProjectXMLReader:
|
|
172
172
|
xml = ET.parse(str(self._path))
|
173
173
|
self._state = XMLReadState.NO_ERROR
|
174
174
|
except Exception as exc:
|
175
|
-
# Trying to open backup file instead
|
176
175
|
logger.error("Failed to parse project XML", exc_info=exc)
|
177
176
|
self._state = XMLReadState.CANNOT_PARSE
|
178
|
-
|
179
|
-
backFile = self._path.with_suffix(".bak")
|
180
|
-
if backFile.is_file():
|
181
|
-
try:
|
182
|
-
xml = ET.parse(str(backFile))
|
183
|
-
self._state = XMLReadState.PARSED_BACKUP
|
184
|
-
logger.info("Backup project file parsed")
|
185
|
-
except Exception as exc:
|
186
|
-
logger.error("Failed to parse backup project XML", exc_info=exc)
|
187
|
-
self._state = XMLReadState.CANNOT_PARSE
|
188
|
-
return False
|
189
|
-
else:
|
190
|
-
self._state = XMLReadState.CANNOT_PARSE
|
191
|
-
return False
|
177
|
+
return False
|
192
178
|
|
193
179
|
xRoot = xml.getroot()
|
194
180
|
self._root = str(xRoot.tag)
|
@@ -248,8 +234,6 @@ class ProjectXMLReader:
|
|
248
234
|
for xItem in xSection:
|
249
235
|
if xItem.tag == "name":
|
250
236
|
data.setName(xItem.text)
|
251
|
-
elif xItem.tag == "title":
|
252
|
-
data.setTitle(xItem.text)
|
253
237
|
elif xItem.tag == "author":
|
254
238
|
data.setAuthor(xItem.text)
|
255
239
|
else:
|
@@ -521,7 +505,6 @@ class ProjectXMLWriter:
|
|
521
505
|
|
522
506
|
xProject = ET.SubElement(xRoot, "project", attrib=projAttr)
|
523
507
|
self._packSingleValue(xProject, "name", data.name)
|
524
|
-
self._packSingleValue(xProject, "title", data.title)
|
525
508
|
self._packSingleValue(xProject, "author", data.author)
|
526
509
|
|
527
510
|
# Save Project Settings
|
@@ -558,23 +541,12 @@ class ProjectXMLWriter:
|
|
558
541
|
xName.text = item["name"]
|
559
542
|
|
560
543
|
# Write the XML tree to file
|
561
|
-
|
562
|
-
tempFile = saveFile.with_suffix(".tmp")
|
563
|
-
backFile = saveFile.with_suffix(".bak")
|
544
|
+
tmp = self._path.with_suffix(".tmp")
|
564
545
|
try:
|
565
546
|
xml = ET.ElementTree(xRoot)
|
566
547
|
xmlIndent(xml)
|
567
|
-
xml.write(
|
568
|
-
|
569
|
-
self._error = exc
|
570
|
-
return False
|
571
|
-
|
572
|
-
# If we're here, the file was successfully saved,
|
573
|
-
# so let's sort out the temps and backups
|
574
|
-
try:
|
575
|
-
if saveFile.exists():
|
576
|
-
saveFile.replace(backFile)
|
577
|
-
tempFile.replace(saveFile)
|
548
|
+
xml.write(tmp, encoding="utf-8", xml_declaration=True)
|
549
|
+
tmp.replace(self._path)
|
578
550
|
except Exception as exc:
|
579
551
|
self._error = exc
|
580
552
|
return False
|
novelwriter/core/sessions.py
CHANGED
@@ -27,8 +27,9 @@ import json
|
|
27
27
|
import logging
|
28
28
|
|
29
29
|
from time import time
|
30
|
-
from typing import TYPE_CHECKING
|
30
|
+
from typing import TYPE_CHECKING
|
31
31
|
from pathlib import Path
|
32
|
+
from collections.abc import Iterator
|
32
33
|
|
33
34
|
from novelwriter.error import logException
|
34
35
|
from novelwriter.common import formatTimeStamp
|
novelwriter/core/spellcheck.py
CHANGED
@@ -27,8 +27,9 @@ from __future__ import annotations
|
|
27
27
|
import json
|
28
28
|
import logging
|
29
29
|
|
30
|
-
from typing import TYPE_CHECKING
|
30
|
+
from typing import TYPE_CHECKING
|
31
31
|
from pathlib import Path
|
32
|
+
from collections.abc import Iterator
|
32
33
|
|
33
34
|
from PyQt5.QtCore import QLocale
|
34
35
|
|
novelwriter/core/status.py
CHANGED
@@ -27,7 +27,8 @@ from __future__ import annotations
|
|
27
27
|
import random
|
28
28
|
import logging
|
29
29
|
|
30
|
-
from typing import TYPE_CHECKING,
|
30
|
+
from typing import TYPE_CHECKING, Literal
|
31
|
+
from collections.abc import ItemsView, Iterator, KeysView, ValuesView
|
31
32
|
|
32
33
|
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
33
34
|
from PyQt5.QtCore import QRectF, Qt
|
novelwriter/core/storage.py
CHANGED
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
26
26
|
import json
|
27
27
|
import logging
|
28
28
|
|
29
|
+
from enum import Enum
|
29
30
|
from time import time
|
30
31
|
from typing import TYPE_CHECKING
|
31
32
|
from pathlib import Path
|
@@ -45,6 +46,26 @@ if TYPE_CHECKING: # pragma: no cover
|
|
45
46
|
logger = logging.getLogger(__name__)
|
46
47
|
|
47
48
|
|
49
|
+
class NWStorageOpen(Enum):
|
50
|
+
|
51
|
+
UNKOWN = 0
|
52
|
+
NOT_FOUND = 1
|
53
|
+
LOCKED = 2
|
54
|
+
FAILED = 3
|
55
|
+
READY = 4
|
56
|
+
|
57
|
+
# END Enum NWStorageOpen
|
58
|
+
|
59
|
+
|
60
|
+
class NWStorageCreate(Enum):
|
61
|
+
|
62
|
+
NOT_EMPTY = 0
|
63
|
+
OS_ERROR = 1
|
64
|
+
READY = 2
|
65
|
+
|
66
|
+
# END Enum NWStorageCreate
|
67
|
+
|
68
|
+
|
48
69
|
class NWStorage:
|
49
70
|
"""Core: Project Storage Class
|
50
71
|
|
@@ -60,7 +81,10 @@ class NWStorage:
|
|
60
81
|
self._storagePath = None
|
61
82
|
self._runtimePath = None
|
62
83
|
self._lockFilePath = None
|
84
|
+
self._lockedBy = None
|
63
85
|
self._openMode = self.MODE_INACTIVE
|
86
|
+
self._ready = False
|
87
|
+
self._exception = None
|
64
88
|
return
|
65
89
|
|
66
90
|
def clear(self) -> None:
|
@@ -69,6 +93,7 @@ class NWStorage:
|
|
69
93
|
self._runtimePath = None
|
70
94
|
self._lockFilePath = None
|
71
95
|
self._openMode = self.MODE_INACTIVE
|
96
|
+
self._ready = False
|
72
97
|
return
|
73
98
|
|
74
99
|
##
|
@@ -77,12 +102,12 @@ class NWStorage:
|
|
77
102
|
|
78
103
|
@property
|
79
104
|
def storagePath(self) -> Path | None:
|
80
|
-
"""
|
105
|
+
"""Return the path where the project is stored."""
|
81
106
|
return self._storagePath
|
82
107
|
|
83
108
|
@property
|
84
109
|
def runtimePath(self) -> Path | None:
|
85
|
-
"""
|
110
|
+
"""Return the path where the project is stored at runtime."""
|
86
111
|
return self._runtimePath
|
87
112
|
|
88
113
|
@property
|
@@ -97,44 +122,127 @@ class NWStorage:
|
|
97
122
|
logger.error("Content path cannot be resolved")
|
98
123
|
return None
|
99
124
|
|
125
|
+
@property
|
126
|
+
def lockStatus(self) -> list | None:
|
127
|
+
"""Return the project lock information."""
|
128
|
+
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
129
|
+
return self._lockedBy
|
130
|
+
return None
|
131
|
+
|
132
|
+
@property
|
133
|
+
def exc(self) -> Exception | None:
|
134
|
+
"""Return the latest exception of the storage instance."""
|
135
|
+
return self._exception
|
136
|
+
|
100
137
|
##
|
101
138
|
# Core Methods
|
102
139
|
##
|
103
140
|
|
104
141
|
def isOpen(self) -> bool:
|
105
142
|
"""Check if the storage location is open."""
|
106
|
-
return self._runtimePath is not None
|
143
|
+
return self._ready and self._runtimePath is not None
|
107
144
|
|
108
|
-
def
|
109
|
-
"""
|
110
|
-
directly from a project folder.
|
111
|
-
"""
|
145
|
+
def createNewProject(self, path: str | Path) -> NWStorageCreate:
|
146
|
+
"""Create a new project at the given location."""
|
112
147
|
inPath = Path(path).resolve()
|
113
|
-
if inPath.
|
114
|
-
|
115
|
-
|
116
|
-
inPath = inPath.parent
|
117
|
-
|
118
|
-
if not (inPath.is_dir() or newProject):
|
119
|
-
# If the project is not new, the folder must already exist.
|
120
|
-
return False
|
148
|
+
if inPath.is_dir() and len(list(inPath.iterdir())) > 0:
|
149
|
+
logger.error("Folder is not empty: %s", inPath)
|
150
|
+
return NWStorageCreate.NOT_EMPTY
|
121
151
|
|
122
152
|
self._storagePath = inPath
|
123
153
|
self._runtimePath = inPath
|
124
154
|
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
|
125
155
|
self._openMode = self.MODE_INPLACE
|
126
156
|
|
127
|
-
|
157
|
+
basePath = self._runtimePath
|
158
|
+
metaPath = basePath / "meta"
|
159
|
+
contPath = basePath / "content"
|
160
|
+
try:
|
161
|
+
basePath.mkdir(exist_ok=True)
|
162
|
+
metaPath.mkdir(exist_ok=True)
|
163
|
+
contPath.mkdir(exist_ok=True)
|
164
|
+
except Exception as exc:
|
165
|
+
self._exception = exc
|
166
|
+
logger.error("Failed to create project folders", exc_info=exc)
|
128
167
|
self.clear()
|
129
|
-
return
|
168
|
+
return NWStorageCreate.OS_ERROR
|
130
169
|
|
131
|
-
|
170
|
+
self._ready = True
|
132
171
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
"""
|
137
|
-
|
172
|
+
return NWStorageCreate.READY
|
173
|
+
|
174
|
+
def initProjectStorage(self, path: str | Path, clearLock: bool = False) -> NWStorageOpen:
|
175
|
+
"""Initialise a novelWriter project location."""
|
176
|
+
inPath = Path(path).resolve()
|
177
|
+
|
178
|
+
# Initialise Storage Instance
|
179
|
+
# ===========================
|
180
|
+
|
181
|
+
# Check what we're opening. Only two options are allowed:
|
182
|
+
# 1. A folder with an nwProject.nwx file in it (not home)
|
183
|
+
# 2. A full path to an nwProject.nwx file
|
184
|
+
if inPath.is_dir() and inPath != Path.home().resolve():
|
185
|
+
nwxFile = inPath / nwFiles.PROJ_FILE
|
186
|
+
elif inPath.is_file():
|
187
|
+
if inPath.name == nwFiles.PROJ_FILE:
|
188
|
+
nwxFile = inPath
|
189
|
+
else:
|
190
|
+
logger.error("Not a novelWriter project")
|
191
|
+
return NWStorageOpen.UNKOWN
|
192
|
+
else:
|
193
|
+
logger.error("Not found: %s", inPath)
|
194
|
+
return NWStorageOpen.NOT_FOUND
|
195
|
+
|
196
|
+
if not nwxFile.exists():
|
197
|
+
# The .nwx file must exist to continue
|
198
|
+
logger.error("Not found: %s", nwxFile)
|
199
|
+
return NWStorageOpen.NOT_FOUND
|
200
|
+
|
201
|
+
nwxPath = nwxFile.parent
|
202
|
+
|
203
|
+
self._storagePath = nwxPath
|
204
|
+
self._runtimePath = nwxPath
|
205
|
+
self._lockFilePath = nwxPath / nwFiles.PROJ_LOCK
|
206
|
+
self._openMode = self.MODE_INPLACE
|
207
|
+
|
208
|
+
# Check Project Lock
|
209
|
+
# ==================
|
210
|
+
|
211
|
+
if clearLock:
|
212
|
+
self._clearLockFile()
|
213
|
+
|
214
|
+
self._readLockFile()
|
215
|
+
if self._lockedBy:
|
216
|
+
logger.error("Project is locked, so not opening")
|
217
|
+
return NWStorageOpen.LOCKED
|
218
|
+
else:
|
219
|
+
logger.debug("Project is not locked")
|
220
|
+
|
221
|
+
# Prepare Folder
|
222
|
+
# ==============
|
223
|
+
|
224
|
+
basePath = self._runtimePath
|
225
|
+
metaPath = basePath / "meta"
|
226
|
+
contPath = basePath / "content"
|
227
|
+
try:
|
228
|
+
metaPath.mkdir(exist_ok=True)
|
229
|
+
contPath.mkdir(exist_ok=True)
|
230
|
+
except Exception as exc:
|
231
|
+
logger.error("Failed to create project folders", exc_info=exc)
|
232
|
+
self.clear()
|
233
|
+
return NWStorageOpen.FAILED
|
234
|
+
|
235
|
+
# Check for legacy data folders
|
236
|
+
legacy = _LegacyStorage(self._project)
|
237
|
+
legacy.deprecatedFiles(basePath)
|
238
|
+
for child in basePath.iterdir():
|
239
|
+
if child.is_dir() and child.name.startswith("data_"):
|
240
|
+
legacy.legacyDataFolder(basePath, child)
|
241
|
+
|
242
|
+
self._writeLockFile()
|
243
|
+
self._ready = True
|
244
|
+
|
245
|
+
return NWStorageOpen.READY
|
138
246
|
|
139
247
|
def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
|
140
248
|
"""Run tasks after the project has been saved.
|
@@ -147,7 +255,7 @@ class NWStorage:
|
|
147
255
|
|
148
256
|
def closeSession(self) -> None:
|
149
257
|
"""Run tasks related to closing the session."""
|
150
|
-
self.
|
258
|
+
self._clearLockFile()
|
151
259
|
self.clear()
|
152
260
|
return
|
153
261
|
|
@@ -157,26 +265,25 @@ class NWStorage:
|
|
157
265
|
|
158
266
|
def getXmlReader(self) -> ProjectXMLReader | None:
|
159
267
|
"""Return a properly configured ProjectXMLReader instance."""
|
160
|
-
if isinstance(self._runtimePath, Path):
|
161
|
-
|
162
|
-
return ProjectXMLReader(projFile)
|
268
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
269
|
+
return ProjectXMLReader(self._runtimePath / nwFiles.PROJ_FILE)
|
163
270
|
return None
|
164
271
|
|
165
272
|
def getXmlWriter(self) -> ProjectXMLWriter | None:
|
166
273
|
"""Return a properly configured ProjectXMLWriter instance."""
|
167
|
-
if isinstance(self._runtimePath, Path):
|
168
|
-
return ProjectXMLWriter(self._runtimePath)
|
274
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
275
|
+
return ProjectXMLWriter(self._runtimePath / nwFiles.PROJ_FILE)
|
169
276
|
return None
|
170
277
|
|
171
278
|
def getDocument(self, tHandle: str | None) -> NWDocument:
|
172
279
|
"""Return a document wrapper object."""
|
173
|
-
if isinstance(self._runtimePath, Path):
|
280
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
174
281
|
return NWDocument(self._project, tHandle)
|
175
282
|
return NWDocument(self._project, None)
|
176
283
|
|
177
284
|
def getMetaFile(self, fileName: str) -> Path | None:
|
178
285
|
"""Return the path to a file in the project meta folder."""
|
179
|
-
if isinstance(self._runtimePath, Path):
|
286
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
180
287
|
return self._runtimePath / "meta" / fileName
|
181
288
|
return None
|
182
289
|
|
@@ -190,59 +297,6 @@ class NWStorage:
|
|
190
297
|
if item.suffix == ".nwd" and isHandle(item.stem)
|
191
298
|
] if contentPath else []
|
192
299
|
|
193
|
-
def readLockFile(self) -> list[str]:
|
194
|
-
"""Read the project lock file."""
|
195
|
-
if self._lockFilePath is None:
|
196
|
-
return ["ERROR"]
|
197
|
-
|
198
|
-
if not self._lockFilePath.exists():
|
199
|
-
return []
|
200
|
-
|
201
|
-
try:
|
202
|
-
lines = self._lockFilePath.read_text(encoding="utf-8").strip().split(";")
|
203
|
-
except Exception:
|
204
|
-
logger.error("Failed to read project lockfile")
|
205
|
-
logException()
|
206
|
-
return ["ERROR"]
|
207
|
-
|
208
|
-
if len(lines) != 4:
|
209
|
-
return ["ERROR"]
|
210
|
-
|
211
|
-
return lines
|
212
|
-
|
213
|
-
def writeLockFile(self) -> bool:
|
214
|
-
"""Write the project lock file."""
|
215
|
-
if self._lockFilePath is None:
|
216
|
-
return False
|
217
|
-
|
218
|
-
data = [
|
219
|
-
CONFIG.hostName, CONFIG.osType,
|
220
|
-
CONFIG.kernelVer, str(int(time()))
|
221
|
-
]
|
222
|
-
try:
|
223
|
-
self._lockFilePath.write_text(";".join(data), encoding="utf-8")
|
224
|
-
except Exception:
|
225
|
-
logger.error("Failed to write project lockfile")
|
226
|
-
logException()
|
227
|
-
return False
|
228
|
-
|
229
|
-
return True
|
230
|
-
|
231
|
-
def clearLockFile(self) -> bool:
|
232
|
-
"""Remove the lock file, if it exists."""
|
233
|
-
if self._lockFilePath is None:
|
234
|
-
return False
|
235
|
-
|
236
|
-
if self._lockFilePath.exists():
|
237
|
-
try:
|
238
|
-
self._lockFilePath.unlink()
|
239
|
-
except Exception:
|
240
|
-
logger.error("Failed to remove project lockfile")
|
241
|
-
logException()
|
242
|
-
return False
|
243
|
-
|
244
|
-
return True
|
245
|
-
|
246
300
|
def zipIt(self, target: str | Path, compression: int | None = None) -> bool:
|
247
301
|
"""Zip the content of the project at its runtime location into a
|
248
302
|
zip file. This process will only grab files that are supposed to
|
@@ -257,7 +311,6 @@ class NWStorage:
|
|
257
311
|
baseCont = basePath / "content"
|
258
312
|
files = [
|
259
313
|
(basePath / nwFiles.PROJ_FILE, nwFiles.PROJ_FILE),
|
260
|
-
(basePath / nwFiles.PROJ_BACKUP, nwFiles.PROJ_BACKUP),
|
261
314
|
(baseMeta / nwFiles.BUILDS_FILE, f"meta/{nwFiles.BUILDS_FILE}"),
|
262
315
|
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
|
263
316
|
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
|
@@ -279,7 +332,7 @@ class NWStorage:
|
|
279
332
|
zipObj.write(srcPath, zipPath)
|
280
333
|
logger.debug("Added: %s", zipPath)
|
281
334
|
except Exception:
|
282
|
-
logger.error("Failed to create
|
335
|
+
logger.error("Failed to create archive")
|
283
336
|
logException()
|
284
337
|
return False
|
285
338
|
|
@@ -289,59 +342,48 @@ class NWStorage:
|
|
289
342
|
# Internal Functions
|
290
343
|
##
|
291
344
|
|
292
|
-
def
|
293
|
-
"""
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
345
|
+
def _readLockFile(self) -> None:
|
346
|
+
"""Read the project lock file."""
|
347
|
+
self._lockedBy = None
|
348
|
+
path = self._lockFilePath
|
349
|
+
if isinstance(path, Path) and path.exists():
|
350
|
+
try:
|
351
|
+
self._lockedBy = path.read_text(encoding="utf-8").strip().split(";")
|
352
|
+
except Exception:
|
353
|
+
logger.error("Failed to read project lockfile")
|
354
|
+
logException()
|
355
|
+
self._lockedBy = ["ERROR", "ERROR", "ERROR", "ERROR"]
|
356
|
+
return
|
357
|
+
return
|
299
358
|
|
300
|
-
|
301
|
-
|
302
|
-
|
359
|
+
def _writeLockFile(self) -> bool:
|
360
|
+
"""Write the project lock file."""
|
361
|
+
if self._lockFilePath is None:
|
303
362
|
return False
|
304
|
-
|
305
|
-
if newProject:
|
306
|
-
# If it's a new project, we check that there is no existing
|
307
|
-
# project in the selected path.
|
308
|
-
if path.exists() and len(list(path.iterdir())) > 0:
|
309
|
-
logger.error("The new project folder is not empty")
|
310
|
-
self.clear()
|
311
|
-
return False
|
312
|
-
|
313
|
-
# The folder is not required to exist, as it could be a new
|
314
|
-
# project, so we make sure it does. Then we add subfolders.
|
315
363
|
try:
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
364
|
+
self._lockFilePath.write_text(
|
365
|
+
f"{CONFIG.hostName};{CONFIG.osType};{CONFIG.kernelVer};{int(time())}",
|
366
|
+
encoding="utf-8"
|
367
|
+
)
|
368
|
+
except Exception:
|
369
|
+
logger.error("Failed to write project lockfile")
|
370
|
+
logException()
|
322
371
|
return False
|
323
|
-
|
324
|
-
if not checkLegacy:
|
325
|
-
# The legacy content check is only needed for project folder
|
326
|
-
# storage, so if it is not expected to be that, there's no
|
327
|
-
# need for the remaining checks.
|
328
|
-
return True
|
329
|
-
|
330
|
-
legacy = _LegacyStorage(self._project)
|
331
|
-
|
332
|
-
# Check for legacy data folders
|
333
|
-
for child in path.iterdir():
|
334
|
-
if child.is_dir() and child.name.startswith("data_"):
|
335
|
-
legacy.legacyDataFolder(path, child)
|
336
|
-
|
337
|
-
# Check for no longer used files, and delete them
|
338
|
-
legacy.deprecatedFiles(path)
|
339
|
-
|
340
372
|
return True
|
341
373
|
|
342
|
-
|
343
|
-
|
344
|
-
|
374
|
+
def _clearLockFile(self) -> bool:
|
375
|
+
"""Remove the lock file, if it exists."""
|
376
|
+
if self._lockFilePath is None:
|
377
|
+
return False
|
378
|
+
if self._lockFilePath.exists():
|
379
|
+
try:
|
380
|
+
self._lockFilePath.unlink()
|
381
|
+
except Exception:
|
382
|
+
logger.error("Failed to remove project lockfile")
|
383
|
+
logException()
|
384
|
+
return False
|
385
|
+
self._lockedBy = None
|
386
|
+
return True
|
345
387
|
|
346
388
|
# END Class NWStorage
|
347
389
|
|
@@ -350,7 +392,7 @@ class _LegacyStorage:
|
|
350
392
|
"""Core: Legacy Storage Converter Utils
|
351
393
|
|
352
394
|
A class with various functions to convert old file formats and
|
353
|
-
file/folder
|
395
|
+
file/folder layouts to the current project format.
|
354
396
|
"""
|
355
397
|
|
356
398
|
def __init__(self, project: NWProject) -> None:
|
@@ -359,7 +401,8 @@ class _LegacyStorage:
|
|
359
401
|
|
360
402
|
def legacyDataFolder(self, path: Path, child: Path) -> None:
|
361
403
|
"""Handle the content of a legacy data folder from a version 1.0
|
362
|
-
project.
|
404
|
+
project. This format had 16 data folders where there now is only
|
405
|
+
one content folder.
|
363
406
|
"""
|
364
407
|
logger.info("Processing legacy data folder: %s", path)
|
365
408
|
|
@@ -411,6 +454,7 @@ class _LegacyStorage:
|
|
411
454
|
path / "meta" / nwFiles.OPTS_FILE
|
412
455
|
)
|
413
456
|
|
457
|
+
# Delete removed files
|
414
458
|
remove = [
|
415
459
|
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
|
416
460
|
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
@@ -422,6 +466,7 @@ class _LegacyStorage:
|
|
422
466
|
path / "cache" / "prevBuild.json", # Dropped in 2.1 Beta 1
|
423
467
|
path / "cache", # Dropped in 2.1 Beta 1
|
424
468
|
path / "ToC.json", # Dropped in 1.0 RC 1
|
469
|
+
path / "nwProject.bak", # Dropped in 2.3 Beta 1
|
425
470
|
]
|
426
471
|
for item in remove:
|
427
472
|
if item.exists():
|
@@ -457,7 +502,6 @@ class _LegacyStorage:
|
|
457
502
|
|
458
503
|
# Save dictionary and clean up old file
|
459
504
|
userDict.save()
|
460
|
-
assert wordJson.exists()
|
461
505
|
wordList.unlink()
|
462
506
|
|
463
507
|
except Exception:
|
@@ -467,9 +511,7 @@ class _LegacyStorage:
|
|
467
511
|
return
|
468
512
|
|
469
513
|
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> None:
|
470
|
-
"""Convert the old text log file format to
|
471
|
-
format.
|
472
|
-
"""
|
514
|
+
"""Convert the old text log file format to JSON Lines."""
|
473
515
|
if sessJson.exists() or not sessLog.exists():
|
474
516
|
# If the new file already exists, we won't overwrite it
|
475
517
|
return
|
@@ -508,7 +550,7 @@ class _LegacyStorage:
|
|
508
550
|
return
|
509
551
|
|
510
552
|
def _convertOldOptionsFile(self, optsOld: Path, optsNew: Path) -> None:
|
511
|
-
"""Convert the old options state file format to
|
553
|
+
"""Convert the old options state file format to new format."""
|
512
554
|
if optsNew.exists() or not optsOld.exists():
|
513
555
|
# If the new file already exists, we won't overwrite it
|
514
556
|
return
|
novelwriter/core/tohtml.py
CHANGED
@@ -338,7 +338,6 @@ class ToHtml(Tokenizer):
|
|
338
338
|
data = {
|
339
339
|
"meta": {
|
340
340
|
"projectName": self._project.data.name,
|
341
|
-
"novelTitle": self._project.data.title,
|
342
341
|
"novelAuthor": self._project.data.author,
|
343
342
|
"buildTime": int(timeStamp),
|
344
343
|
"buildTimeStr": formatTimeStamp(timeStamp),
|
@@ -485,7 +484,10 @@ class ToHtml(Tokenizer):
|
|
485
484
|
result = f"<span class='tags'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
486
485
|
if len(bits) > 1:
|
487
486
|
if bits[0] == nwKeyWords.TAG_KEY:
|
488
|
-
|
487
|
+
one, two = self._project.index.parseValue(bits[1])
|
488
|
+
result += f"<a name='tag_{one}'>{one}</a>"
|
489
|
+
if two:
|
490
|
+
result += f" | <span class='optional'>{two}</a>"
|
489
491
|
else:
|
490
492
|
if self._genMode == self.M_PREVIEW:
|
491
493
|
result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
|