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.
Files changed (153) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +141 -129
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +11 -6
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  13. novelwriter/assets/i18n/project_de_DE.json +1 -0
  14. novelwriter/assets/i18n/project_en_US.json +1 -0
  15. novelwriter/assets/i18n/project_es_419.json +11 -0
  16. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  17. novelwriter/assets/i18n/project_it_IT.json +11 -0
  18. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  19. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  20. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  21. novelwriter/assets/icons/typicons_dark/icons.conf +9 -2
  22. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  25. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  27. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  29. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  30. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  31. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  32. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  33. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  34. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  35. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/icons.conf +9 -2
  37. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  40. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  42. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  44. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  45. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  46. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  47. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  48. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  50. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  51. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  52. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  53. novelwriter/assets/images/welcome-dark.jpg +0 -0
  54. novelwriter/assets/images/welcome-light.jpg +0 -0
  55. novelwriter/assets/manual.pdf +0 -0
  56. novelwriter/assets/sample.zip +0 -0
  57. novelwriter/assets/syntax/default_dark.conf +1 -0
  58. novelwriter/assets/syntax/default_light.conf +1 -0
  59. novelwriter/assets/syntax/grey_dark.conf +1 -0
  60. novelwriter/assets/syntax/grey_light.conf +1 -0
  61. novelwriter/assets/syntax/light_owl.conf +1 -0
  62. novelwriter/assets/syntax/night_owl.conf +1 -0
  63. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  64. novelwriter/assets/syntax/solarized_light.conf +1 -0
  65. novelwriter/assets/syntax/tomorrow.conf +1 -0
  66. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  67. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  68. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  69. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  70. novelwriter/assets/text/credits_en.htm +4 -2
  71. novelwriter/assets/themes/default_dark.conf +2 -2
  72. novelwriter/assets/themes/default_light.conf +2 -2
  73. novelwriter/common.py +64 -66
  74. novelwriter/config.py +39 -44
  75. novelwriter/constants.py +39 -17
  76. novelwriter/core/buildsettings.py +8 -8
  77. novelwriter/core/coretools.py +194 -155
  78. novelwriter/core/docbuild.py +7 -4
  79. novelwriter/core/document.py +7 -7
  80. novelwriter/core/index.py +90 -57
  81. novelwriter/core/item.py +23 -5
  82. novelwriter/core/options.py +11 -10
  83. novelwriter/core/project.py +72 -47
  84. novelwriter/core/projectdata.py +3 -16
  85. novelwriter/core/projectxml.py +14 -42
  86. novelwriter/core/sessions.py +4 -3
  87. novelwriter/core/spellcheck.py +6 -4
  88. novelwriter/core/status.py +5 -4
  89. novelwriter/core/storage.py +179 -141
  90. novelwriter/core/tohtml.py +6 -4
  91. novelwriter/core/tokenizer.py +74 -46
  92. novelwriter/core/tomd.py +2 -2
  93. novelwriter/core/toodt.py +41 -31
  94. novelwriter/core/tree.py +5 -4
  95. novelwriter/dialogs/about.py +88 -179
  96. novelwriter/dialogs/docmerge.py +30 -20
  97. novelwriter/dialogs/docsplit.py +33 -22
  98. novelwriter/dialogs/editlabel.py +20 -8
  99. novelwriter/dialogs/preferences.py +562 -725
  100. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  101. novelwriter/dialogs/quotes.py +47 -36
  102. novelwriter/dialogs/wordlist.py +128 -59
  103. novelwriter/enum.py +25 -22
  104. novelwriter/error.py +2 -2
  105. novelwriter/extensions/circularprogress.py +12 -12
  106. novelwriter/extensions/configlayout.py +185 -146
  107. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  108. novelwriter/extensions/modified.py +81 -0
  109. novelwriter/extensions/novelselector.py +27 -13
  110. novelwriter/extensions/pagedsidebar.py +15 -20
  111. novelwriter/extensions/simpleprogress.py +8 -9
  112. novelwriter/extensions/statusled.py +9 -9
  113. novelwriter/extensions/switch.py +32 -64
  114. novelwriter/extensions/switchbox.py +2 -7
  115. novelwriter/extensions/versioninfo.py +153 -0
  116. novelwriter/gui/doceditor.py +250 -214
  117. novelwriter/gui/dochighlight.py +66 -94
  118. novelwriter/gui/docviewer.py +71 -98
  119. novelwriter/gui/docviewerpanel.py +140 -47
  120. novelwriter/gui/editordocument.py +3 -3
  121. novelwriter/gui/itemdetails.py +9 -9
  122. novelwriter/gui/mainmenu.py +47 -46
  123. novelwriter/gui/noveltree.py +53 -61
  124. novelwriter/gui/outline.py +100 -76
  125. novelwriter/gui/projtree.py +193 -67
  126. novelwriter/gui/sidebar.py +9 -8
  127. novelwriter/gui/statusbar.py +49 -7
  128. novelwriter/gui/theme.py +65 -74
  129. novelwriter/guimain.py +173 -330
  130. novelwriter/shared.py +68 -30
  131. novelwriter/tools/dictionaries.py +7 -8
  132. novelwriter/tools/lipsum.py +34 -28
  133. novelwriter/tools/manusbuild.py +3 -4
  134. novelwriter/tools/manuscript.py +25 -32
  135. novelwriter/tools/manussettings.py +194 -225
  136. novelwriter/tools/noveldetails.py +525 -0
  137. novelwriter/tools/welcome.py +802 -0
  138. novelwriter/tools/writingstats.py +26 -13
  139. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  140. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  141. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  142. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  143. novelwriter/assets/images/wizard-back.jpg +0 -0
  144. novelwriter/assets/text/gplv3_en.htm +0 -641
  145. novelwriter/assets/text/release_notes.htm +0 -17
  146. novelwriter/dialogs/projdetails.py +0 -525
  147. novelwriter/dialogs/projload.py +0 -298
  148. novelwriter/dialogs/updates.py +0 -182
  149. novelwriter/extensions/pageddialog.py +0 -130
  150. novelwriter/tools/projwizard.py +0 -478
  151. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  152. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  153. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
@@ -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–2023, Veronica Berglyd Olsen
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
- """Get the path where the project is saved."""
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
- """Get the path where the project is saved at runtime."""
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 openProjectInPlace(self, path: str | Path, newProject: bool = False) -> bool:
109
- """Open a novelWriter project in-place. That is, it is opened
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.is_file():
114
- # The path should not point to an existing file,
115
- # but it can point to a folder containing files
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
- if not self._prepareStorage(checkLegacy=True, newProject=newProject):
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 False
168
+ return NWStorageCreate.OS_ERROR
130
169
 
131
- return True
170
+ self._ready = True
132
171
 
133
- def openProjectArchive(self, path: str | Path) -> bool: # pragma: no cover
134
- """Open the project from a single file.
135
- Placeholder for later implementation. See #977.
136
- """
137
- return False
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.clearLockFile()
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
- projFile = self._runtimePath / nwFiles.PROJ_FILE
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 acrhive")
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 _prepareStorage(self, checkLegacy: bool = True, newProject: bool = False) -> bool:
293
- """Prepare the storage area for the project."""
294
- path = self._runtimePath
295
- if not isinstance(path, Path):
296
- logger.error("No path set")
297
- self.clear()
298
- return False
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
- if path == Path.home().absolute():
301
- logger.error("Cannot use the user's home path as the root of a project")
302
- self.clear()
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
- path.mkdir(exist_ok=True)
317
- (path / "content").mkdir(exist_ok=True)
318
- (path / "meta").mkdir(exist_ok=True)
319
- except Exception as exc:
320
- logger.error("Failed to create required project folders", exc_info=exc)
321
- self.clear()
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
- # Legacy Project Data Handlers
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 layout to the current project format.
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 the new JSON Lines
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 the format."""
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
@@ -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–2023, Veronica Berglyd Olsen
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
- result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
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:])