novelWriter 2.5.2__py3-none-any.whl → 2.6__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 (129) hide show
  1. {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/METADATA +5 -4
  2. {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/RECORD +126 -105
  3. {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +50 -11
  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_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  16. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  17. novelwriter/assets/i18n/project_de_DE.json +4 -2
  18. novelwriter/assets/i18n/project_en_GB.json +1 -0
  19. novelwriter/assets/i18n/project_en_US.json +2 -0
  20. novelwriter/assets/i18n/project_it_IT.json +2 -0
  21. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  22. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  23. novelwriter/assets/i18n/project_nl_NL.json +2 -0
  24. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  25. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  26. novelwriter/assets/i18n/project_ru_RU.json +11 -0
  27. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  28. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  29. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  30. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  31. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  32. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  33. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  34. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  35. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  36. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  37. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  38. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  40. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  41. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  42. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  43. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  44. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  45. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  46. novelwriter/assets/manual.pdf +0 -0
  47. novelwriter/assets/sample.zip +0 -0
  48. novelwriter/assets/text/credits_en.htm +1 -0
  49. novelwriter/assets/themes/default_light.conf +2 -2
  50. novelwriter/common.py +101 -3
  51. novelwriter/config.py +30 -17
  52. novelwriter/constants.py +189 -81
  53. novelwriter/core/buildsettings.py +74 -40
  54. novelwriter/core/coretools.py +146 -148
  55. novelwriter/core/docbuild.py +133 -171
  56. novelwriter/core/document.py +1 -1
  57. novelwriter/core/index.py +39 -38
  58. novelwriter/core/item.py +42 -9
  59. novelwriter/core/itemmodel.py +518 -0
  60. novelwriter/core/options.py +5 -2
  61. novelwriter/core/project.py +68 -90
  62. novelwriter/core/projectdata.py +8 -2
  63. novelwriter/core/projectxml.py +1 -1
  64. novelwriter/core/sessions.py +1 -1
  65. novelwriter/core/spellcheck.py +10 -15
  66. novelwriter/core/status.py +24 -8
  67. novelwriter/core/storage.py +1 -1
  68. novelwriter/core/tree.py +269 -288
  69. novelwriter/dialogs/about.py +1 -1
  70. novelwriter/dialogs/docmerge.py +8 -18
  71. novelwriter/dialogs/docsplit.py +1 -1
  72. novelwriter/dialogs/editlabel.py +1 -1
  73. novelwriter/dialogs/preferences.py +47 -34
  74. novelwriter/dialogs/projectsettings.py +149 -99
  75. novelwriter/dialogs/quotes.py +1 -1
  76. novelwriter/dialogs/wordlist.py +11 -10
  77. novelwriter/enum.py +37 -24
  78. novelwriter/error.py +2 -2
  79. novelwriter/extensions/configlayout.py +28 -13
  80. novelwriter/extensions/eventfilters.py +1 -1
  81. novelwriter/extensions/modified.py +30 -6
  82. novelwriter/extensions/novelselector.py +4 -3
  83. novelwriter/extensions/pagedsidebar.py +9 -9
  84. novelwriter/extensions/progressbars.py +4 -4
  85. novelwriter/extensions/statusled.py +3 -3
  86. novelwriter/extensions/switch.py +3 -3
  87. novelwriter/extensions/switchbox.py +1 -1
  88. novelwriter/extensions/versioninfo.py +1 -1
  89. novelwriter/formats/shared.py +156 -0
  90. novelwriter/formats/todocx.py +1191 -0
  91. novelwriter/formats/tohtml.py +454 -0
  92. novelwriter/{core → formats}/tokenizer.py +497 -495
  93. novelwriter/formats/tomarkdown.py +218 -0
  94. novelwriter/{core → formats}/toodt.py +312 -433
  95. novelwriter/formats/toqdoc.py +486 -0
  96. novelwriter/formats/toraw.py +91 -0
  97. novelwriter/gui/doceditor.py +347 -287
  98. novelwriter/gui/dochighlight.py +97 -85
  99. novelwriter/gui/docviewer.py +90 -33
  100. novelwriter/gui/docviewerpanel.py +18 -26
  101. novelwriter/gui/editordocument.py +18 -3
  102. novelwriter/gui/itemdetails.py +27 -29
  103. novelwriter/gui/mainmenu.py +130 -64
  104. novelwriter/gui/noveltree.py +46 -48
  105. novelwriter/gui/outline.py +202 -256
  106. novelwriter/gui/projtree.py +590 -1238
  107. novelwriter/gui/search.py +11 -19
  108. novelwriter/gui/sidebar.py +8 -7
  109. novelwriter/gui/statusbar.py +20 -3
  110. novelwriter/gui/theme.py +11 -6
  111. novelwriter/guimain.py +101 -201
  112. novelwriter/shared.py +67 -28
  113. novelwriter/text/counting.py +3 -1
  114. novelwriter/text/patterns.py +169 -61
  115. novelwriter/tools/dictionaries.py +3 -3
  116. novelwriter/tools/lipsum.py +1 -1
  117. novelwriter/tools/manusbuild.py +15 -13
  118. novelwriter/tools/manuscript.py +121 -79
  119. novelwriter/tools/manussettings.py +424 -291
  120. novelwriter/tools/noveldetails.py +1 -1
  121. novelwriter/tools/welcome.py +6 -6
  122. novelwriter/tools/writingstats.py +4 -4
  123. novelwriter/types.py +25 -9
  124. novelwriter/core/tohtml.py +0 -530
  125. novelwriter/core/tomarkdown.py +0 -252
  126. novelwriter/core/toqdoc.py +0 -419
  127. {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/LICENSE.md +0 -0
  128. {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/entry_points.txt +0 -0
  129. {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ Created: 2022-11-03 [2.0rc2] ProjectBuilder
9
9
  Created: 2023-07-20 [2.1b1] DocDuplicator
10
10
 
11
11
  This file is a part of novelWriter
12
- Copyright 2018–2024, Veronica Berglyd Olsen
12
+ Copyright (C) 2022 Veronica Berglyd Olsen and novelWriter contributors
13
13
 
14
14
  This program is free software: you can redistribute it and/or modify
15
15
  it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
27
27
  from __future__ import annotations
28
28
 
29
29
  import logging
30
+ import re
30
31
  import shutil
31
32
 
32
33
  from collections.abc import Iterable
@@ -34,11 +35,11 @@ from functools import partial
34
35
  from pathlib import Path
35
36
  from zipfile import ZipFile, is_zipfile
36
37
 
37
- from PyQt5.QtCore import QCoreApplication, QRegularExpression
38
+ from PyQt5.QtCore import QCoreApplication
38
39
 
39
40
  from novelwriter import CONFIG, SHARED
40
41
  from novelwriter.common import isHandle, minmax, simplified
41
- from novelwriter.constants import nwConst, nwFiles, nwItemClass
42
+ from novelwriter.constants import nwConst, nwFiles, nwItemClass, nwStats
42
43
  from novelwriter.core.item import NWItem
43
44
  from novelwriter.core.project import NWProject
44
45
  from novelwriter.core.storage import NWStorageCreate
@@ -55,10 +56,17 @@ class DocMerger:
55
56
  def __init__(self, project: NWProject) -> None:
56
57
  self._project = project
57
58
  self._error = ""
58
- self._targetDoc = None
59
- self._targetText = []
59
+ self._target = None
60
+ self._text = []
60
61
  return
61
62
 
63
+ @property
64
+ def targetHandle(self) -> str | None:
65
+ """Get the handle of the target document."""
66
+ if self._target:
67
+ return self._target.itemHandle
68
+ return None
69
+
62
70
  ##
63
71
  # Methods
64
72
  ##
@@ -71,63 +79,56 @@ class DocMerger:
71
79
  """Set the target document for the merging. Calling this
72
80
  function resets the class.
73
81
  """
74
- self._targetDoc = tHandle
75
- self._targetText = []
82
+ self._target = self._project.tree[tHandle]
83
+ self._text = []
76
84
  return
77
85
 
78
- def newTargetDoc(self, srcHandle: str, docLabel: str) -> str | None:
86
+ def newTargetDoc(self, sHandle: str, label: str) -> None:
79
87
  """Create a brand new target document based on a source handle
80
88
  and a new doc label. Calling this function resets the class.
81
89
  """
82
- srcItem = self._project.tree[srcHandle]
83
- if srcItem is None or srcItem.itemParent is None:
84
- return None
85
-
86
- newHandle = self._project.newFile(docLabel, srcItem.itemParent)
87
- newItem = self._project.tree[newHandle]
88
- if isinstance(newItem, NWItem):
89
- newItem.setLayout(srcItem.itemLayout)
90
- newItem.setStatus(srcItem.itemStatus)
91
- newItem.setImport(srcItem.itemImport)
92
-
93
- self._targetDoc = newHandle
94
- self._targetText = []
95
-
96
- return newHandle
90
+ sItem = self._project.tree[sHandle]
91
+ if sItem and sItem.itemParent:
92
+ tHandle = self._project.newFile(label, sItem.itemParent)
93
+ if nwItem := self._project.tree[tHandle]:
94
+ nwItem.setLayout(sItem.itemLayout)
95
+ nwItem.setStatus(sItem.itemStatus)
96
+ nwItem.setImport(sItem.itemImport)
97
+ nwItem.notifyToRefresh()
98
+ self._target = nwItem
99
+ self._text = []
100
+ return
97
101
 
98
- def appendText(self, srcHandle: str, addComment: bool, cmtPrefix: str) -> bool:
102
+ def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None:
99
103
  """Append text from an existing document to the text buffer."""
100
- srcItem = self._project.tree[srcHandle]
101
- if srcItem is None:
102
- return False
103
-
104
- docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n")
105
- if addComment:
106
- docInfo = srcItem.describeMe()
107
- docSt, _ = srcItem.getImportStatus()
108
- cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
109
- docText = cmtLine + docText
110
-
111
- self._targetText.append(docText)
112
-
113
- return True
104
+ if item := self._project.tree[sHandle]:
105
+ text = self._project.storage.getDocumentText(sHandle).rstrip("\n")
106
+ if addComment:
107
+ info = item.describeMe()
108
+ status, _ = item.getImportStatus()
109
+ text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}"
110
+ self._text.append(text)
111
+ return
114
112
 
115
113
  def writeTargetDoc(self) -> bool:
116
114
  """Write the accumulated text into the designated target
117
115
  document, appending any existing text.
118
116
  """
119
- if self._targetDoc is None:
120
- return False
117
+ if self._target:
118
+ outDoc = self._project.storage.getDocument(self._target.itemHandle)
119
+ if text := (outDoc.readDocument() or "").rstrip("\n"):
120
+ self._text.insert(0, text)
121
+
122
+ status = outDoc.writeDocument("\n\n".join(self._text) + "\n\n")
123
+ if not status:
124
+ self._error = outDoc.getError()
121
125
 
122
- outDoc = self._project.storage.getDocument(self._targetDoc)
123
- if text := (outDoc.readDocument() or "").rstrip("\n"):
124
- self._targetText.insert(0, text)
126
+ self._project.index.reIndexHandle(self._target.itemHandle)
127
+ self._target.notifyToRefresh()
125
128
 
126
- status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
127
- if not status:
128
- self._error = outDoc.getError()
129
+ return status
129
130
 
130
- return status
131
+ return False
131
132
 
132
133
 
133
134
  class DocSplitter:
@@ -171,23 +172,19 @@ class DocSplitter:
171
172
  self._inFolder = False
172
173
  return
173
174
 
174
- def newParentFolder(self, pHandle: str, folderLabel: str) -> str | None:
175
+ def newParentFolder(self, pHandle: str, folderLabel: str) -> None:
175
176
  """Create a new folder that will be the top level parent item
176
177
  for the new documents.
177
178
  """
178
- if self._srcItem is None:
179
- return None
180
-
181
- newHandle = self._project.newFolder(folderLabel, pHandle)
182
- newItem = self._project.tree[newHandle]
183
- if isinstance(newItem, NWItem):
184
- newItem.setStatus(self._srcItem.itemStatus)
185
- newItem.setImport(self._srcItem.itemImport)
186
-
187
- self._parHandle = newHandle
188
- self._inFolder = True
189
-
190
- return newHandle
179
+ if self._srcItem:
180
+ nHandle = self._project.newFolder(folderLabel, pHandle)
181
+ if nwItem := self._project.tree[nHandle]:
182
+ nwItem.setStatus(self._srcItem.itemStatus)
183
+ nwItem.setImport(self._srcItem.itemImport)
184
+ nwItem.notifyToRefresh()
185
+ self._parHandle = nHandle
186
+ self._inFolder = True
187
+ return
191
188
 
192
189
  def splitDocument(self, splitData: list, splitText: list[str]) -> None:
193
190
  """Loop through the split data record and perform the split job
@@ -201,58 +198,50 @@ class DocSplitter:
201
198
  self._rawData.insert(0, (chunk, hLevel, hLabel))
202
199
  return
203
200
 
204
- def writeDocuments(self, docHierarchy: bool) -> Iterable[tuple[bool, str | None, str | None]]:
201
+ def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]:
205
202
  """An iterator that will write each document in the buffer, and
206
203
  return its new handle, parent handle, and sibling handle.
207
204
  """
208
- if self._srcHandle is None or self._srcItem is None or self._parHandle is None:
209
- return
210
-
211
- pHandle = self._parHandle
212
- nHandle = self._parHandle if self._inFolder else self._srcHandle
213
- hHandle = [self._parHandle, None, None, None, None]
214
-
215
- pLevel = 0
216
- for docText, hLevel, docLabel in self._rawData:
217
-
218
- hLevel = minmax(hLevel, 1, 4)
219
- if pLevel == 0:
220
- pLevel = hLevel
221
-
222
- if docHierarchy:
223
- if hLevel == 1:
224
- pHandle = self._parHandle
225
- elif hLevel == 2:
226
- pHandle = hHandle[1] or hHandle[0]
227
- elif hLevel == 3:
228
- pHandle = hHandle[2] or hHandle[1] or hHandle[0]
229
- elif hLevel == 4:
230
- pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
231
-
232
- if hLevel < pLevel:
233
- nHandle = hHandle[hLevel] or hHandle[0]
234
- elif hLevel > pLevel:
235
- nHandle = pHandle
236
-
237
- dHandle = self._project.newFile(docLabel, pHandle)
238
- hHandle[hLevel] = dHandle
239
-
240
- newItem = self._project.tree[dHandle]
241
- if isinstance(newItem, NWItem):
242
- newItem.setStatus(self._srcItem.itemStatus)
243
- newItem.setImport(self._srcItem.itemImport)
244
-
245
- outDoc = self._project.storage.getDocument(dHandle)
246
- status = outDoc.writeDocument("\n".join(docText))
247
- if not status:
248
- self._error = outDoc.getError()
249
-
250
- yield status, dHandle, nHandle
251
-
252
- hHandle[hLevel] = dHandle
253
- nHandle = dHandle
254
- pLevel = hLevel
255
-
205
+ if self._srcHandle and self._srcItem and self._parHandle:
206
+ pHandle = self._parHandle
207
+ hHandle = [self._parHandle, None, None, None, None]
208
+ pLevel = 0
209
+ for docText, hLevel, docLabel in self._rawData:
210
+
211
+ hLevel = minmax(hLevel, 1, 4)
212
+ if pLevel == 0:
213
+ pLevel = hLevel
214
+
215
+ if docHierarchy:
216
+ if hLevel == 1:
217
+ pHandle = self._parHandle
218
+ elif hLevel == 2:
219
+ pHandle = hHandle[1] or hHandle[0]
220
+ elif hLevel == 3:
221
+ pHandle = hHandle[2] or hHandle[1] or hHandle[0]
222
+ elif hLevel == 4:
223
+ pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
224
+
225
+ if (
226
+ (dHandle := self._project.newFile(docLabel, pHandle))
227
+ and (nwItem := self._project.tree[dHandle])
228
+ ):
229
+ hHandle[hLevel] = dHandle
230
+ nwItem.setStatus(self._srcItem.itemStatus)
231
+ nwItem.setImport(self._srcItem.itemImport)
232
+
233
+ outDoc = self._project.storage.getDocument(dHandle)
234
+ status = outDoc.writeDocument("\n".join(docText))
235
+ if not status:
236
+ self._error = outDoc.getError()
237
+
238
+ self._project.index.reIndexHandle(dHandle)
239
+ nwItem.notifyToRefresh()
240
+
241
+ yield status
242
+
243
+ hHandle[hLevel] = dHandle
244
+ pLevel = hLevel
256
245
  return
257
246
 
258
247
 
@@ -269,36 +258,34 @@ class DocDuplicator:
269
258
  # Methods
270
259
  ##
271
260
 
272
- def duplicate(self, items: list[str]) -> Iterable[tuple[str, str | None]]:
261
+ def duplicate(self, items: list[str]) -> list[str]:
273
262
  """Run through a list of items, duplicate them, and copy the
274
263
  text content if they are documents.
275
264
  """
265
+ result = []
266
+ after = True
276
267
  if items:
277
- nHandle = items[0]
278
268
  hMap: dict[str, str | None] = {t: None for t in items}
279
269
  for tHandle in items:
280
- newItem = self._project.tree.duplicate(tHandle)
281
- if newItem is None:
282
- return
283
- hMap[tHandle] = newItem.itemHandle
284
- if newItem.itemParent in hMap:
285
- newItem.setParent(hMap[newItem.itemParent])
286
- self._project.tree.updateItemData(newItem.itemHandle)
287
- if newItem.isFileType():
288
- newDoc = self._project.storage.getDocument(newItem.itemHandle)
289
- if newDoc.fileExists():
290
- return
291
- newDoc.writeDocument(self._project.storage.getDocumentText(tHandle))
292
- yield newItem.itemHandle, nHandle
293
- nHandle = None
294
- return
270
+ if oldItem := self._project.tree[tHandle]:
271
+ pHandle = hMap.get(oldItem.itemParent or "") or oldItem.itemParent
272
+ if newItem := self._project.tree.duplicate(tHandle, pHandle, after):
273
+ hMap[tHandle] = newItem.itemHandle
274
+ if newItem.isFileType():
275
+ self._project.copyFileContent(newItem.itemHandle, tHandle)
276
+ newItem.notifyToRefresh()
277
+ result.append(newItem.itemHandle)
278
+ after = False
279
+ else:
280
+ break
281
+ return result
295
282
 
296
283
 
297
284
  class DocSearch:
298
285
 
299
286
  def __init__(self) -> None:
300
- self._regEx = QRegularExpression()
301
- self.setCaseSensitive(False)
287
+ self._regEx = re.compile("")
288
+ self._opts = re.UNICODE | re.IGNORECASE
302
289
  self._words = False
303
290
  self._escape = True
304
291
  return
@@ -309,10 +296,9 @@ class DocSearch:
309
296
 
310
297
  def setCaseSensitive(self, state: bool) -> None:
311
298
  """Set the case sensitive search flag."""
312
- opts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
299
+ self._opts = re.UNICODE
313
300
  if not state:
314
- opts |= QRegularExpression.PatternOption.CaseInsensitiveOption
315
- self._regEx.setPatternOptions(opts)
301
+ self._opts |= re.IGNORECASE
316
302
  return
317
303
 
318
304
  def setWholeWords(self, state: bool) -> None:
@@ -329,8 +315,8 @@ class DocSearch:
329
315
  self, project: NWProject, search: str
330
316
  ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
331
317
  """Iteratively search through documents in a project."""
332
- self._regEx.setPattern(self._buildPattern(search))
333
- logger.debug("Searching with pattern '%s'", self._regEx.pattern())
318
+ self._regEx = re.compile(self._buildPattern(search), self._opts)
319
+ logger.debug("Searching with pattern '%s'", self._regEx.pattern)
334
320
  storage = project.storage
335
321
  for item in project.tree:
336
322
  if item.isFileType():
@@ -340,14 +326,12 @@ class DocSearch:
340
326
 
341
327
  def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
342
328
  """Search a piece of text for RegEx matches."""
343
- rxItt = self._regEx.globalMatch(text)
344
329
  count = 0
345
330
  capped = False
346
331
  results = []
347
- while rxItt.hasNext():
348
- rxMatch = rxItt.next()
349
- pos = rxMatch.capturedStart()
350
- num = rxMatch.capturedLength()
332
+ for res in self._regEx.finditer(text):
333
+ pos = res.start(0)
334
+ num = len(res.group(0))
351
335
  lim = text[:pos].rfind("\n") + 1
352
336
  cut = text[lim:pos].rfind(" ") + lim + 1
353
337
  context = text[cut:cut+100].partition("\n")[0]
@@ -366,7 +350,7 @@ class DocSearch:
366
350
  def _buildPattern(self, search: str) -> str:
367
351
  """Build the search pattern string."""
368
352
  if self._escape:
369
- search = QRegularExpression.escape(search)
353
+ search = re.escape(search)
370
354
  if self._words:
371
355
  search = f"(?:^|\\b){search}(?:$|\\b)"
372
356
  return search
@@ -430,7 +414,6 @@ class ProjectBuilder:
430
414
 
431
415
  lblNewProject = self.tr("New Project")
432
416
  lblTitlePage = self.tr("Title Page")
433
- lblByAuthors = self.tr("By")
434
417
 
435
418
  # Settings
436
419
  project.data.setUuid(None)
@@ -443,14 +426,29 @@ class ProjectBuilder:
443
426
  # Add Root Folders
444
427
  hNovelRoot = project.newRoot(nwItemClass.NOVEL)
445
428
  hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
446
- novelTitle = project.data.name
447
-
448
- titlePage = f"#! {novelTitle}\n\n"
449
- if project.data.author:
450
- titlePage += f">> {lblByAuthors} {project.data.author} <<\n\n"
451
429
 
430
+ # Generate Title Page
452
431
  aDoc = project.storage.getDocument(hTitlePage)
453
- aDoc.writeDocument(titlePage)
432
+ aDoc.writeDocument((
433
+ "{author}[br]\n"
434
+ "{address} 1[br]\n"
435
+ "{address} 2 <<\n"
436
+ "\n"
437
+ "[vspace:5]\n"
438
+ "\n"
439
+ "#! {title}\n"
440
+ "\n"
441
+ ">> **{by} {author}** <<\n"
442
+ "\n"
443
+ ">> {count}: [field:{field}] <<\n"
444
+ ).format(
445
+ author=project.data.author or "None",
446
+ address=self.tr("Address"),
447
+ title=project.data.name or "None",
448
+ by=self.tr("By"),
449
+ count=self.tr("Word Count"),
450
+ field=nwStats.WORDS_TEXT,
451
+ ))
454
452
 
455
453
  # Create a project structure based on selected root folders
456
454
  # and a number of chapters and scenes selected in the
@@ -511,7 +509,7 @@ class ProjectBuilder:
511
509
 
512
510
  # Also add the archive and trash folders
513
511
  project.newRoot(nwItemClass.ARCHIVE)
514
- project.trashFolder()
512
+ project.tree.trash # Triggers the creation of Trash
515
513
 
516
514
  project.saveProject()
517
515
  project.closeProject()