novelWriter 2.2rc1__py3-none-any.whl → 2.3b1__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.2rc1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
- {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +141 -129
- {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +11 -6
- 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_fr_FR.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_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +1 -0
- novelwriter/assets/i18n/project_en_US.json +1 -0
- novelwriter/assets/i18n/project_es_419.json +11 -0
- novelwriter/assets/i18n/project_fr_FR.json +11 -0
- novelwriter/assets/i18n/project_it_IT.json +11 -0
- novelwriter/assets/i18n/project_ja_JP.json +2 -1
- novelwriter/assets/i18n/project_nb_NO.json +1 -0
- novelwriter/assets/i18n/project_zh_CN.json +11 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +9 -2
- novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
- novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
- novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
- novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
- novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -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_light/icons.conf +9 -2
- novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
- novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
- novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
- novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
- novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -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/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/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/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/default_dark.conf +2 -2
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +64 -66
- novelwriter/config.py +39 -44
- novelwriter/constants.py +39 -17
- novelwriter/core/buildsettings.py +8 -8
- novelwriter/core/coretools.py +194 -155
- novelwriter/core/docbuild.py +7 -4
- novelwriter/core/document.py +7 -7
- novelwriter/core/index.py +90 -57
- novelwriter/core/item.py +23 -5
- novelwriter/core/options.py +11 -10
- novelwriter/core/project.py +72 -47
- novelwriter/core/projectdata.py +3 -16
- novelwriter/core/projectxml.py +14 -42
- novelwriter/core/sessions.py +4 -3
- novelwriter/core/spellcheck.py +6 -4
- novelwriter/core/status.py +5 -4
- novelwriter/core/storage.py +179 -141
- novelwriter/core/tohtml.py +6 -4
- novelwriter/core/tokenizer.py +74 -46
- novelwriter/core/tomd.py +2 -2
- novelwriter/core/toodt.py +41 -31
- novelwriter/core/tree.py +5 -4
- novelwriter/dialogs/about.py +88 -179
- novelwriter/dialogs/docmerge.py +30 -20
- novelwriter/dialogs/docsplit.py +33 -22
- novelwriter/dialogs/editlabel.py +20 -8
- novelwriter/dialogs/preferences.py +562 -725
- novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
- novelwriter/dialogs/quotes.py +47 -36
- novelwriter/dialogs/wordlist.py +128 -59
- novelwriter/enum.py +25 -22
- novelwriter/error.py +2 -2
- novelwriter/extensions/circularprogress.py +12 -12
- novelwriter/extensions/configlayout.py +185 -146
- novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
- novelwriter/extensions/modified.py +81 -0
- novelwriter/extensions/novelselector.py +27 -13
- novelwriter/extensions/pagedsidebar.py +15 -20
- novelwriter/extensions/simpleprogress.py +8 -9
- novelwriter/extensions/statusled.py +9 -9
- novelwriter/extensions/switch.py +32 -64
- novelwriter/extensions/switchbox.py +2 -7
- novelwriter/extensions/versioninfo.py +153 -0
- novelwriter/gui/doceditor.py +250 -214
- novelwriter/gui/dochighlight.py +66 -94
- novelwriter/gui/docviewer.py +71 -98
- novelwriter/gui/docviewerpanel.py +140 -47
- novelwriter/gui/editordocument.py +3 -3
- novelwriter/gui/itemdetails.py +9 -9
- novelwriter/gui/mainmenu.py +47 -46
- novelwriter/gui/noveltree.py +53 -61
- novelwriter/gui/outline.py +100 -76
- novelwriter/gui/projtree.py +193 -67
- novelwriter/gui/sidebar.py +9 -8
- novelwriter/gui/statusbar.py +49 -7
- novelwriter/gui/theme.py +65 -74
- novelwriter/guimain.py +173 -330
- novelwriter/shared.py +68 -30
- novelwriter/tools/dictionaries.py +7 -8
- novelwriter/tools/lipsum.py +34 -28
- novelwriter/tools/manusbuild.py +3 -4
- novelwriter/tools/manuscript.py +25 -32
- novelwriter/tools/manussettings.py +194 -225
- novelwriter/tools/noveldetails.py +525 -0
- novelwriter/tools/welcome.py +802 -0
- novelwriter/tools/writingstats.py +26 -13
- novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
- novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
- novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
- novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
- novelwriter/assets/images/wizard-back.jpg +0 -0
- novelwriter/assets/text/gplv3_en.htm +0 -641
- novelwriter/assets/text/release_notes.htm +0 -17
- novelwriter/dialogs/projdetails.py +0 -525
- novelwriter/dialogs/projload.py +0 -298
- novelwriter/dialogs/updates.py +0 -182
- novelwriter/extensions/pageddialog.py +0 -130
- novelwriter/tools/projwizard.py +0 -478
- {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
novelwriter/core/storage.py
CHANGED
@@ -6,7 +6,7 @@ File History:
|
|
6
6
|
Created: 2022-11-01 [2.0rc2] NWStorage
|
7
7
|
|
8
8
|
This file is a part of novelWriter
|
9
|
-
Copyright 2018–
|
9
|
+
Copyright 2018–2024, Veronica Berglyd Olsen
|
10
10
|
|
11
11
|
This program is free software: you can redistribute it and/or modify
|
12
12
|
it under the terms of the GNU General Public License as published by
|
@@ -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,123 @@ 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() and inPath.name == nwFiles.PROJ_FILE:
|
187
|
+
nwxFile = inPath
|
188
|
+
else:
|
189
|
+
logger.error("Not a novelWriter project")
|
190
|
+
return NWStorageOpen.UNKOWN
|
191
|
+
|
192
|
+
if not nwxFile.exists():
|
193
|
+
# The .nwx file must exist to continue
|
194
|
+
logger.error("Not found: %s", nwxFile)
|
195
|
+
return NWStorageOpen.NOT_FOUND
|
196
|
+
|
197
|
+
nwxPath = nwxFile.parent
|
198
|
+
|
199
|
+
self._storagePath = nwxPath
|
200
|
+
self._runtimePath = nwxPath
|
201
|
+
self._lockFilePath = nwxPath / nwFiles.PROJ_LOCK
|
202
|
+
self._openMode = self.MODE_INPLACE
|
203
|
+
|
204
|
+
# Check Project Lock
|
205
|
+
# ==================
|
206
|
+
|
207
|
+
if clearLock:
|
208
|
+
self._clearLockFile()
|
209
|
+
|
210
|
+
self._readLockFile()
|
211
|
+
if self._lockedBy:
|
212
|
+
logger.error("Project is locked, so not opening")
|
213
|
+
return NWStorageOpen.LOCKED
|
214
|
+
else:
|
215
|
+
logger.debug("Project is not locked")
|
216
|
+
|
217
|
+
# Prepare Folder
|
218
|
+
# ==============
|
219
|
+
|
220
|
+
basePath = self._runtimePath
|
221
|
+
metaPath = basePath / "meta"
|
222
|
+
contPath = basePath / "content"
|
223
|
+
try:
|
224
|
+
metaPath.mkdir(exist_ok=True)
|
225
|
+
contPath.mkdir(exist_ok=True)
|
226
|
+
except Exception as exc:
|
227
|
+
logger.error("Failed to create project folders", exc_info=exc)
|
228
|
+
self.clear()
|
229
|
+
return NWStorageOpen.FAILED
|
230
|
+
|
231
|
+
# Check for legacy data folders
|
232
|
+
legacy = _LegacyStorage(self._project)
|
233
|
+
legacy.deprecatedFiles(basePath)
|
234
|
+
for child in basePath.iterdir():
|
235
|
+
if child.is_dir() and child.name.startswith("data_"):
|
236
|
+
legacy.legacyDataFolder(basePath, child)
|
237
|
+
|
238
|
+
self._writeLockFile()
|
239
|
+
self._ready = True
|
240
|
+
|
241
|
+
return NWStorageOpen.READY
|
138
242
|
|
139
243
|
def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
|
140
244
|
"""Run tasks after the project has been saved.
|
@@ -147,7 +251,7 @@ class NWStorage:
|
|
147
251
|
|
148
252
|
def closeSession(self) -> None:
|
149
253
|
"""Run tasks related to closing the session."""
|
150
|
-
self.
|
254
|
+
self._clearLockFile()
|
151
255
|
self.clear()
|
152
256
|
return
|
153
257
|
|
@@ -157,26 +261,25 @@ class NWStorage:
|
|
157
261
|
|
158
262
|
def getXmlReader(self) -> ProjectXMLReader | None:
|
159
263
|
"""Return a properly configured ProjectXMLReader instance."""
|
160
|
-
if isinstance(self._runtimePath, Path):
|
161
|
-
|
162
|
-
return ProjectXMLReader(projFile)
|
264
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
265
|
+
return ProjectXMLReader(self._runtimePath / nwFiles.PROJ_FILE)
|
163
266
|
return None
|
164
267
|
|
165
268
|
def getXmlWriter(self) -> ProjectXMLWriter | None:
|
166
269
|
"""Return a properly configured ProjectXMLWriter instance."""
|
167
|
-
if isinstance(self._runtimePath, Path):
|
168
|
-
return ProjectXMLWriter(self._runtimePath)
|
270
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
271
|
+
return ProjectXMLWriter(self._runtimePath / nwFiles.PROJ_FILE)
|
169
272
|
return None
|
170
273
|
|
171
274
|
def getDocument(self, tHandle: str | None) -> NWDocument:
|
172
275
|
"""Return a document wrapper object."""
|
173
|
-
if isinstance(self._runtimePath, Path):
|
276
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
174
277
|
return NWDocument(self._project, tHandle)
|
175
278
|
return NWDocument(self._project, None)
|
176
279
|
|
177
280
|
def getMetaFile(self, fileName: str) -> Path | None:
|
178
281
|
"""Return the path to a file in the project meta folder."""
|
179
|
-
if isinstance(self._runtimePath, Path):
|
282
|
+
if isinstance(self._runtimePath, Path) and self._ready:
|
180
283
|
return self._runtimePath / "meta" / fileName
|
181
284
|
return None
|
182
285
|
|
@@ -190,59 +293,6 @@ class NWStorage:
|
|
190
293
|
if item.suffix == ".nwd" and isHandle(item.stem)
|
191
294
|
] if contentPath else []
|
192
295
|
|
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
296
|
def zipIt(self, target: str | Path, compression: int | None = None) -> bool:
|
247
297
|
"""Zip the content of the project at its runtime location into a
|
248
298
|
zip file. This process will only grab files that are supposed to
|
@@ -257,7 +307,6 @@ class NWStorage:
|
|
257
307
|
baseCont = basePath / "content"
|
258
308
|
files = [
|
259
309
|
(basePath / nwFiles.PROJ_FILE, nwFiles.PROJ_FILE),
|
260
|
-
(basePath / nwFiles.PROJ_BACKUP, nwFiles.PROJ_BACKUP),
|
261
310
|
(baseMeta / nwFiles.BUILDS_FILE, f"meta/{nwFiles.BUILDS_FILE}"),
|
262
311
|
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
|
263
312
|
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
|
@@ -279,7 +328,7 @@ class NWStorage:
|
|
279
328
|
zipObj.write(srcPath, zipPath)
|
280
329
|
logger.debug("Added: %s", zipPath)
|
281
330
|
except Exception:
|
282
|
-
logger.error("Failed to create
|
331
|
+
logger.error("Failed to create archive")
|
283
332
|
logException()
|
284
333
|
return False
|
285
334
|
|
@@ -289,59 +338,48 @@ class NWStorage:
|
|
289
338
|
# Internal Functions
|
290
339
|
##
|
291
340
|
|
292
|
-
def
|
293
|
-
"""
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
341
|
+
def _readLockFile(self) -> None:
|
342
|
+
"""Read the project lock file."""
|
343
|
+
self._lockedBy = None
|
344
|
+
path = self._lockFilePath
|
345
|
+
if isinstance(path, Path) and path.exists():
|
346
|
+
try:
|
347
|
+
self._lockedBy = path.read_text(encoding="utf-8").strip().split(";")
|
348
|
+
except Exception:
|
349
|
+
logger.error("Failed to read project lockfile")
|
350
|
+
logException()
|
351
|
+
self._lockedBy = ["ERROR", "ERROR", "ERROR", "ERROR"]
|
352
|
+
return
|
353
|
+
return
|
299
354
|
|
300
|
-
|
301
|
-
|
302
|
-
|
355
|
+
def _writeLockFile(self) -> bool:
|
356
|
+
"""Write the project lock file."""
|
357
|
+
if self._lockFilePath is None:
|
303
358
|
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
359
|
try:
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
360
|
+
self._lockFilePath.write_text(
|
361
|
+
f"{CONFIG.hostName};{CONFIG.osType};{CONFIG.kernelVer};{int(time())}",
|
362
|
+
encoding="utf-8"
|
363
|
+
)
|
364
|
+
except Exception:
|
365
|
+
logger.error("Failed to write project lockfile")
|
366
|
+
logException()
|
322
367
|
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
368
|
return True
|
341
369
|
|
342
|
-
|
343
|
-
|
344
|
-
|
370
|
+
def _clearLockFile(self) -> bool:
|
371
|
+
"""Remove the lock file, if it exists."""
|
372
|
+
if self._lockFilePath is None:
|
373
|
+
return False
|
374
|
+
if self._lockFilePath.exists():
|
375
|
+
try:
|
376
|
+
self._lockFilePath.unlink()
|
377
|
+
except Exception:
|
378
|
+
logger.error("Failed to remove project lockfile")
|
379
|
+
logException()
|
380
|
+
return False
|
381
|
+
self._lockedBy = None
|
382
|
+
return True
|
345
383
|
|
346
384
|
# END Class NWStorage
|
347
385
|
|
@@ -350,7 +388,7 @@ class _LegacyStorage:
|
|
350
388
|
"""Core: Legacy Storage Converter Utils
|
351
389
|
|
352
390
|
A class with various functions to convert old file formats and
|
353
|
-
file/folder
|
391
|
+
file/folder layouts to the current project format.
|
354
392
|
"""
|
355
393
|
|
356
394
|
def __init__(self, project: NWProject) -> None:
|
@@ -359,7 +397,8 @@ class _LegacyStorage:
|
|
359
397
|
|
360
398
|
def legacyDataFolder(self, path: Path, child: Path) -> None:
|
361
399
|
"""Handle the content of a legacy data folder from a version 1.0
|
362
|
-
project.
|
400
|
+
project. This format had 16 data folders where there now is only
|
401
|
+
one content folder.
|
363
402
|
"""
|
364
403
|
logger.info("Processing legacy data folder: %s", path)
|
365
404
|
|
@@ -411,6 +450,7 @@ class _LegacyStorage:
|
|
411
450
|
path / "meta" / nwFiles.OPTS_FILE
|
412
451
|
)
|
413
452
|
|
453
|
+
# Delete removed files
|
414
454
|
remove = [
|
415
455
|
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
|
416
456
|
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
@@ -422,6 +462,7 @@ class _LegacyStorage:
|
|
422
462
|
path / "cache" / "prevBuild.json", # Dropped in 2.1 Beta 1
|
423
463
|
path / "cache", # Dropped in 2.1 Beta 1
|
424
464
|
path / "ToC.json", # Dropped in 1.0 RC 1
|
465
|
+
path / "nwProject.bak", # Dropped in 2.3 Beta 1
|
425
466
|
]
|
426
467
|
for item in remove:
|
427
468
|
if item.exists():
|
@@ -457,7 +498,6 @@ class _LegacyStorage:
|
|
457
498
|
|
458
499
|
# Save dictionary and clean up old file
|
459
500
|
userDict.save()
|
460
|
-
assert wordJson.exists()
|
461
501
|
wordList.unlink()
|
462
502
|
|
463
503
|
except Exception:
|
@@ -467,9 +507,7 @@ class _LegacyStorage:
|
|
467
507
|
return
|
468
508
|
|
469
509
|
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> None:
|
470
|
-
"""Convert the old text log file format to
|
471
|
-
format.
|
472
|
-
"""
|
510
|
+
"""Convert the old text log file format to JSON Lines."""
|
473
511
|
if sessJson.exists() or not sessLog.exists():
|
474
512
|
# If the new file already exists, we won't overwrite it
|
475
513
|
return
|
@@ -508,7 +546,7 @@ class _LegacyStorage:
|
|
508
546
|
return
|
509
547
|
|
510
548
|
def _convertOldOptionsFile(self, optsOld: Path, optsNew: Path) -> None:
|
511
|
-
"""Convert the old options state file format to
|
549
|
+
"""Convert the old options state file format to new format."""
|
512
550
|
if optsNew.exists() or not optsOld.exists():
|
513
551
|
# If the new file already exists, we won't overwrite it
|
514
552
|
return
|
novelwriter/core/tohtml.py
CHANGED
@@ -3,10 +3,10 @@ novelWriter – HTML Text Converter
|
|
3
3
|
=================================
|
4
4
|
|
5
5
|
File History:
|
6
|
-
Created: 2019-05-07 [0.0.1]
|
6
|
+
Created: 2019-05-07 [0.0.1] ToHtml
|
7
7
|
|
8
8
|
This file is a part of novelWriter
|
9
|
-
Copyright 2018–
|
9
|
+
Copyright 2018–2024, Veronica Berglyd Olsen
|
10
10
|
|
11
11
|
This program is free software: you can redistribute it and/or modify
|
12
12
|
it under the terms of the GNU General Public License as published by
|
@@ -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:])
|