novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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 (83) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
  3. novelwriter/__init__.py +49 -10
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  6. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  8. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  9. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  10. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  14. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  17. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  18. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  23. novelwriter/assets/manual.pdf +0 -0
  24. novelwriter/assets/sample.zip +0 -0
  25. novelwriter/common.py +100 -2
  26. novelwriter/config.py +25 -15
  27. novelwriter/constants.py +168 -60
  28. novelwriter/core/buildsettings.py +66 -39
  29. novelwriter/core/coretools.py +145 -147
  30. novelwriter/core/docbuild.py +132 -170
  31. novelwriter/core/index.py +38 -37
  32. novelwriter/core/item.py +41 -8
  33. novelwriter/core/itemmodel.py +518 -0
  34. novelwriter/core/options.py +4 -1
  35. novelwriter/core/project.py +67 -89
  36. novelwriter/core/spellcheck.py +9 -14
  37. novelwriter/core/status.py +7 -5
  38. novelwriter/core/tree.py +268 -287
  39. novelwriter/dialogs/docmerge.py +7 -17
  40. novelwriter/dialogs/preferences.py +46 -33
  41. novelwriter/dialogs/projectsettings.py +5 -5
  42. novelwriter/enum.py +36 -23
  43. novelwriter/extensions/configlayout.py +27 -12
  44. novelwriter/extensions/modified.py +13 -1
  45. novelwriter/extensions/pagedsidebar.py +5 -5
  46. novelwriter/formats/shared.py +155 -0
  47. novelwriter/formats/todocx.py +1191 -0
  48. novelwriter/formats/tohtml.py +451 -0
  49. novelwriter/{core → formats}/tokenizer.py +487 -491
  50. novelwriter/formats/tomarkdown.py +217 -0
  51. novelwriter/{core → formats}/toodt.py +311 -432
  52. novelwriter/formats/toqdoc.py +484 -0
  53. novelwriter/formats/toraw.py +91 -0
  54. novelwriter/gui/doceditor.py +342 -284
  55. novelwriter/gui/dochighlight.py +96 -84
  56. novelwriter/gui/docviewer.py +88 -31
  57. novelwriter/gui/docviewerpanel.py +17 -25
  58. novelwriter/gui/editordocument.py +17 -2
  59. novelwriter/gui/itemdetails.py +25 -28
  60. novelwriter/gui/mainmenu.py +129 -63
  61. novelwriter/gui/noveltree.py +45 -47
  62. novelwriter/gui/outline.py +196 -249
  63. novelwriter/gui/projtree.py +594 -1241
  64. novelwriter/gui/search.py +9 -10
  65. novelwriter/gui/sidebar.py +7 -6
  66. novelwriter/gui/theme.py +10 -5
  67. novelwriter/guimain.py +100 -196
  68. novelwriter/shared.py +66 -27
  69. novelwriter/text/counting.py +2 -0
  70. novelwriter/text/patterns.py +168 -60
  71. novelwriter/tools/manusbuild.py +14 -12
  72. novelwriter/tools/manuscript.py +120 -78
  73. novelwriter/tools/manussettings.py +424 -291
  74. novelwriter/tools/welcome.py +4 -4
  75. novelwriter/tools/writingstats.py +3 -3
  76. novelwriter/types.py +23 -7
  77. novelwriter/core/tohtml.py +0 -530
  78. novelwriter/core/tomarkdown.py +0 -252
  79. novelwriter/core/toqdoc.py +0 -419
  80. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  81. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
  82. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  83. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
@@ -36,7 +36,7 @@ from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
36
36
 
37
37
  from novelwriter import CONFIG
38
38
  from novelwriter.common import checkUuid, isHandle, jsonEncode
39
- from novelwriter.constants import nwFiles, nwHeadFmt
39
+ from novelwriter.constants import nwFiles, nwHeadFmt, nwStyles
40
40
  from novelwriter.core.project import NWProject
41
41
  from novelwriter.enum import nwBuildFmt
42
42
  from novelwriter.error import logException
@@ -45,29 +45,30 @@ logger = logging.getLogger(__name__)
45
45
 
46
46
  # The Settings Template
47
47
  # =====================
48
- # Each entry contains a tuple on the form:
49
- # (type, default, [min value, max value])
48
+ # Each entry contains a tuple on the form: (type, default)
50
49
 
51
- SETTINGS_TEMPLATE = {
50
+ SETTINGS_TEMPLATE: dict[str, tuple[type, str | int | float | bool]] = {
52
51
  "filter.includeNovel": (bool, True),
53
52
  "filter.includeNotes": (bool, False),
54
53
  "filter.includeInactive": (bool, False),
55
- "headings.fmtTitle": (str, nwHeadFmt.TITLE),
54
+ "headings.fmtPart": (str, nwHeadFmt.TITLE),
56
55
  "headings.fmtChapter": (str, nwHeadFmt.TITLE),
57
56
  "headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
58
57
  "headings.fmtScene": (str, "* * *"),
59
58
  "headings.fmtAltScene": (str, ""),
60
59
  "headings.fmtSection": (str, ""),
61
- "headings.hideTitle": (bool, False),
60
+ "headings.hidePart": (bool, False),
62
61
  "headings.hideChapter": (bool, False),
63
62
  "headings.hideUnnumbered": (bool, False),
64
63
  "headings.hideScene": (bool, False),
65
64
  "headings.hideAltScene": (bool, False),
66
65
  "headings.hideSection": (bool, True),
67
66
  "headings.centerTitle": (bool, True),
67
+ "headings.centerPart": (bool, True),
68
68
  "headings.centerChapter": (bool, False),
69
69
  "headings.centerScene": (bool, False),
70
- "headings.breakTitle": (bool, True),
70
+ "headings.breakTitle": (bool, False),
71
+ "headings.breakPart": (bool, True),
71
72
  "headings.breakChapter": (bool, True),
72
73
  "headings.breakScene": (bool, False),
73
74
  "text.includeSynopsis": (bool, False),
@@ -77,7 +78,7 @@ SETTINGS_TEMPLATE = {
77
78
  "text.ignoredKeywords": (str, ""),
78
79
  "text.addNoteHeadings": (bool, True),
79
80
  "format.textFont": (str, CONFIG.textFont.toString()),
80
- "format.lineHeight": (float, 1.15, 0.75, 3.0),
81
+ "format.lineHeight": (float, 1.15),
81
82
  "format.justifyText": (bool, False),
82
83
  "format.stripUnicode": (bool, False),
83
84
  "format.replaceTabs": (bool, False),
@@ -86,6 +87,20 @@ SETTINGS_TEMPLATE = {
86
87
  "format.firstLineIndent": (bool, False),
87
88
  "format.firstIndentWidth": (float, 1.4),
88
89
  "format.indentFirstPar": (bool, False),
90
+ "format.titleMarginT": (float, nwStyles.T_MARGIN["H0"][0]),
91
+ "format.titleMarginB": (float, nwStyles.T_MARGIN["H0"][1]),
92
+ "format.h1MarginT": (float, nwStyles.T_MARGIN["H1"][0]),
93
+ "format.h1MarginB": (float, nwStyles.T_MARGIN["H1"][1]),
94
+ "format.h2MarginT": (float, nwStyles.T_MARGIN["H2"][0]),
95
+ "format.h2MarginB": (float, nwStyles.T_MARGIN["H2"][1]),
96
+ "format.h3MarginT": (float, nwStyles.T_MARGIN["H3"][0]),
97
+ "format.h3MarginB": (float, nwStyles.T_MARGIN["H3"][1]),
98
+ "format.h4MarginT": (float, nwStyles.T_MARGIN["H4"][0]),
99
+ "format.h4MarginB": (float, nwStyles.T_MARGIN["H4"][1]),
100
+ "format.textMarginT": (float, nwStyles.T_MARGIN["TT"][0]),
101
+ "format.textMarginB": (float, nwStyles.T_MARGIN["TT"][1]),
102
+ "format.sepMarginT": (float, nwStyles.T_MARGIN["SP"][0]),
103
+ "format.sepMarginB": (float, nwStyles.T_MARGIN["SP"][1]),
89
104
  "format.pageUnit": (str, "cm"),
90
105
  "format.pageSize": (str, "A4"),
91
106
  "format.pageWidth": (float, 21.0),
@@ -94,9 +109,11 @@ SETTINGS_TEMPLATE = {
94
109
  "format.bottomMargin": (float, 2.0),
95
110
  "format.leftMargin": (float, 2.0),
96
111
  "format.rightMargin": (float, 2.0),
97
- "odt.addColours": (bool, True),
98
- "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
99
- "odt.pageCountOffset": (int, 0),
112
+ "doc.pageHeader": (str, nwHeadFmt.DOC_AUTO),
113
+ "doc.pageCountOffset": (int, 0),
114
+ "doc.colorHeadings": (bool, True),
115
+ "doc.scaleHeadings": (bool, True),
116
+ "doc.boldHeadings": (bool, True),
100
117
  "html.addStyles": (bool, True),
101
118
  "html.preserveTabs": (bool, False),
102
119
  }
@@ -108,12 +125,16 @@ SETTINGS_LABELS = {
108
125
  "filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
109
126
 
110
127
  "headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
111
- "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
128
+ "headings.fmtPart": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
112
129
  "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
113
130
  "headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
114
131
  "headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
115
132
  "headings.fmtAltScene": QT_TRANSLATE_NOOP("Builds", "Alt. Scene Format"),
116
133
  "headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
134
+ "headings.styleTitle": QT_TRANSLATE_NOOP("Builds", "Title Styling"),
135
+ "headings.stylePart": QT_TRANSLATE_NOOP("Builds", "Partition Styling"),
136
+ "headings.styleChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Styling"),
137
+ "headings.styleScene": QT_TRANSLATE_NOOP("Builds", "Scene Styling"),
117
138
 
118
139
  "text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
119
140
  "text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
@@ -121,13 +142,11 @@ SETTINGS_LABELS = {
121
142
  "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
122
143
  "text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
123
144
  "text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
124
- "text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
125
145
  "text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
126
146
 
127
147
  "format.grpFormat": QT_TRANSLATE_NOOP("Builds", "Text Format"),
128
148
  "format.textFont": QT_TRANSLATE_NOOP("Builds", "Text Font"),
129
149
  "format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"),
130
- "format.grpOptions": QT_TRANSLATE_NOOP("Builds", "Text Options"),
131
150
  "format.justifyText": QT_TRANSLATE_NOOP("Builds", "Justify Text Margins"),
132
151
  "format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
133
152
  "format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
@@ -139,26 +158,31 @@ SETTINGS_LABELS = {
139
158
  "format.firstIndentWidth": QT_TRANSLATE_NOOP("Builds", "Indent Width"),
140
159
  "format.indentFirstPar": QT_TRANSLATE_NOOP("Builds", "Indent First Paragraph"),
141
160
 
161
+ "format.grpMargins": QT_TRANSLATE_NOOP("Builds", "Text Margins"),
162
+
142
163
  "format.grpPage": QT_TRANSLATE_NOOP("Builds", "Page Layout"),
143
164
  "format.pageUnit": QT_TRANSLATE_NOOP("Builds", "Unit"),
144
165
  "format.pageSize": QT_TRANSLATE_NOOP("Builds", "Page Size"),
145
- "format.pageWidth": QT_TRANSLATE_NOOP("Builds", "Page Width"),
146
- "format.pageHeight": QT_TRANSLATE_NOOP("Builds", "Page Height"),
147
- "format.topMargin": QT_TRANSLATE_NOOP("Builds", "Top Margin"),
148
- "format.bottomMargin": QT_TRANSLATE_NOOP("Builds", "Bottom Margin"),
149
- "format.leftMargin": QT_TRANSLATE_NOOP("Builds", "Left Margin"),
150
- "format.rightMargin": QT_TRANSLATE_NOOP("Builds", "Right Margin"),
151
-
152
- "odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"),
153
- "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
154
- "odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
155
- "odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
156
-
157
- "html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
166
+ "format.pageMargins": QT_TRANSLATE_NOOP("Builds", "Page Margins"),
167
+
168
+ "doc": QT_TRANSLATE_NOOP("Builds", "Document Style"),
169
+ "doc.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
170
+ "doc.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
171
+ "doc.colorHeadings": QT_TRANSLATE_NOOP("Builds", "Add Colours to Headings"),
172
+ "doc.scaleHeadings": QT_TRANSLATE_NOOP("Builds", "Increase Size of Headings"),
173
+ "doc.boldHeadings": QT_TRANSLATE_NOOP("Builds", "Bold Headings"),
174
+
175
+ "html": QT_TRANSLATE_NOOP("Builds", "HTML Options"),
158
176
  "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
159
177
  "html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
160
178
  }
161
179
 
180
+ RENAMED = {
181
+ "odt.addColours": "doc.addColours",
182
+ "odt.pageHeader": "doc.pageHeader",
183
+ "odt.pageCountOffset": "doc.pageCountOffset",
184
+ }
185
+
162
186
 
163
187
  class FilterMode(Enum):
164
188
  """The decision reason for an item in a filtered project."""
@@ -348,18 +372,12 @@ class BuildSettings:
348
372
  self._changed = True
349
373
  return
350
374
 
351
- def setValue(self, key: str, value: str | int | bool | float) -> bool:
375
+ def setValue(self, key: str, value: str | int | float | bool) -> None:
352
376
  """Set a specific value for a build setting."""
353
- if key not in SETTINGS_TEMPLATE:
354
- return False
355
- definition = SETTINGS_TEMPLATE[key]
356
- if not isinstance(value, definition[0]):
357
- return False
358
- if len(definition) == 4 and isinstance(value, (int, float)):
359
- value = min(max(value, definition[2]), definition[3])
360
- self._changed = value != self._settings[key]
361
- self._settings[key] = value
362
- return True
377
+ if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
378
+ self._changed = value != self._settings[key]
379
+ self._settings[key] = value
380
+ return
363
381
 
364
382
  ##
365
383
  # Methods
@@ -478,12 +496,21 @@ class BuildSettings:
478
496
  self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
479
497
  if isinstance(settings, dict):
480
498
  for key, value in settings.items():
481
- self.setValue(key, value)
499
+ self.setValue(RENAMED.get(key, key), value)
482
500
 
483
501
  self._changed = False
484
502
 
485
503
  return
486
504
 
505
+ @classmethod
506
+ def duplicate(cls, source: BuildSettings) -> BuildSettings:
507
+ """Make a copy of another build."""
508
+ cls = BuildSettings()
509
+ cls.unpack(source.pack())
510
+ cls._uuid = str(uuid.uuid4())
511
+ cls._name = f"{source.name} 2"
512
+ return cls
513
+
487
514
 
488
515
  class BuildCollection:
489
516
  """Core: Build Collection Class
@@ -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()