novelWriter 2.3rc1__py3-none-any.whl → 2.4__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 (125) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/RECORD +119 -109
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +17 -10
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  17. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  18. novelwriter/assets/icons/none.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  21. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  23. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  25. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  28. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  29. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  31. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  33. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  35. novelwriter/assets/manual.pdf +0 -0
  36. novelwriter/assets/sample.zip +0 -0
  37. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  38. novelwriter/assets/syntax/default_dark.conf +1 -0
  39. novelwriter/assets/syntax/default_light.conf +1 -0
  40. novelwriter/assets/syntax/grey_dark.conf +1 -0
  41. novelwriter/assets/syntax/grey_light.conf +1 -0
  42. novelwriter/assets/syntax/light_owl.conf +1 -0
  43. novelwriter/assets/syntax/night_owl.conf +1 -0
  44. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  45. novelwriter/assets/syntax/solarized_light.conf +1 -0
  46. novelwriter/assets/syntax/tango.conf +23 -0
  47. novelwriter/assets/syntax/tomorrow.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  49. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  50. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  51. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  52. novelwriter/assets/text/credits_en.htm +25 -23
  53. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  54. novelwriter/common.py +12 -4
  55. novelwriter/config.py +47 -16
  56. novelwriter/constants.py +5 -6
  57. novelwriter/core/buildsettings.py +64 -44
  58. novelwriter/core/coretools.py +97 -13
  59. novelwriter/core/docbuild.py +74 -7
  60. novelwriter/core/document.py +24 -3
  61. novelwriter/core/index.py +31 -112
  62. novelwriter/core/project.py +11 -15
  63. novelwriter/core/projectxml.py +3 -2
  64. novelwriter/core/sessions.py +2 -2
  65. novelwriter/core/spellcheck.py +3 -3
  66. novelwriter/core/status.py +6 -5
  67. novelwriter/core/storage.py +16 -6
  68. novelwriter/core/tohtml.py +22 -25
  69. novelwriter/core/tokenizer.py +417 -237
  70. novelwriter/core/tomd.py +17 -8
  71. novelwriter/core/toodt.py +386 -351
  72. novelwriter/core/tree.py +8 -8
  73. novelwriter/dialogs/about.py +10 -12
  74. novelwriter/dialogs/docmerge.py +17 -14
  75. novelwriter/dialogs/docsplit.py +20 -19
  76. novelwriter/dialogs/editlabel.py +5 -4
  77. novelwriter/dialogs/preferences.py +32 -40
  78. novelwriter/dialogs/projectsettings.py +31 -28
  79. novelwriter/dialogs/quotes.py +10 -9
  80. novelwriter/dialogs/wordlist.py +18 -15
  81. novelwriter/enum.py +17 -14
  82. novelwriter/error.py +14 -12
  83. novelwriter/extensions/circularprogress.py +12 -8
  84. novelwriter/extensions/configlayout.py +23 -3
  85. novelwriter/extensions/modified.py +51 -2
  86. novelwriter/extensions/pagedsidebar.py +16 -14
  87. novelwriter/extensions/simpleprogress.py +3 -1
  88. novelwriter/extensions/statusled.py +3 -1
  89. novelwriter/extensions/switch.py +10 -9
  90. novelwriter/extensions/switchbox.py +14 -13
  91. novelwriter/extensions/versioninfo.py +1 -1
  92. novelwriter/gui/doceditor.py +433 -496
  93. novelwriter/gui/dochighlight.py +54 -33
  94. novelwriter/gui/docviewer.py +162 -175
  95. novelwriter/gui/docviewerpanel.py +20 -37
  96. novelwriter/gui/editordocument.py +15 -4
  97. novelwriter/gui/itemdetails.py +51 -54
  98. novelwriter/gui/mainmenu.py +37 -17
  99. novelwriter/gui/noveltree.py +31 -37
  100. novelwriter/gui/outline.py +120 -98
  101. novelwriter/gui/projtree.py +114 -112
  102. novelwriter/gui/search.py +362 -0
  103. novelwriter/gui/sidebar.py +36 -45
  104. novelwriter/gui/statusbar.py +14 -14
  105. novelwriter/gui/theme.py +116 -34
  106. novelwriter/guimain.py +216 -207
  107. novelwriter/shared.py +31 -6
  108. novelwriter/text/counting.py +138 -0
  109. novelwriter/tools/dictionaries.py +15 -14
  110. novelwriter/tools/lipsum.py +20 -17
  111. novelwriter/tools/manusbuild.py +43 -35
  112. novelwriter/tools/manuscript.py +381 -104
  113. novelwriter/tools/manussettings.py +263 -125
  114. novelwriter/tools/noveldetails.py +21 -19
  115. novelwriter/tools/welcome.py +59 -57
  116. novelwriter/tools/writingstats.py +61 -55
  117. novelwriter/types.py +90 -0
  118. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  119. novelwriter/core/__init__.py +0 -3
  120. novelwriter/dialogs/__init__.py +0 -3
  121. novelwriter/extensions/__init__.py +0 -3
  122. novelwriter/gui/__init__.py +0 -3
  123. novelwriter/tools/__init__.py +0 -3
  124. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
  125. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
@@ -49,39 +49,54 @@ logger = logging.getLogger(__name__)
49
49
  # (type, default, [min value, max value])
50
50
 
51
51
  SETTINGS_TEMPLATE = {
52
- "filter.includeNovel": (bool, True),
53
- "filter.includeNotes": (bool, False),
54
- "filter.includeInactive": (bool, False),
55
- "headings.fmtTitle": (str, nwHeadFmt.TITLE),
56
- "headings.fmtChapter": (str, nwHeadFmt.TITLE),
57
- "headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
58
- "headings.fmtScene": (str, "* * *"),
59
- "headings.fmtSection": (str, ""),
60
- "headings.hideScene": (bool, False),
61
- "headings.hideSection": (bool, True),
62
- "text.includeSynopsis": (bool, False),
63
- "text.includeComments": (bool, False),
64
- "text.includeKeywords": (bool, False),
65
- "text.includeBodyText": (bool, True),
66
- "text.addNoteHeadings": (bool, True),
67
- "format.textFont": (str, CONFIG.textFont),
68
- "format.textSize": (int, 12),
69
- "format.lineHeight": (float, 1.15, 0.75, 3.0),
70
- "format.justifyText": (bool, False),
71
- "format.stripUnicode": (bool, False),
72
- "format.replaceTabs": (bool, False),
73
- "format.pageUnit": (str, "cm"),
74
- "format.pageSize": (str, "A4"),
75
- "format.pageWidth": (float, 21.0),
76
- "format.pageHeight": (float, 29.7),
77
- "format.topMargin": (float, 2.0),
78
- "format.bottomMargin": (float, 2.0),
79
- "format.leftMargin": (float, 2.0),
80
- "format.rightMargin": (float, 2.0),
81
- "odt.addColours": (bool, True),
82
- "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
83
- "odt.pageCountOffset": (int, 0),
84
- "html.addStyles": (bool, True),
52
+ "filter.includeNovel": (bool, True),
53
+ "filter.includeNotes": (bool, False),
54
+ "filter.includeInactive": (bool, False),
55
+ "headings.fmtTitle": (str, nwHeadFmt.TITLE),
56
+ "headings.fmtChapter": (str, nwHeadFmt.TITLE),
57
+ "headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
58
+ "headings.fmtScene": (str, "* * *"),
59
+ "headings.fmtAltScene": (str, ""),
60
+ "headings.fmtSection": (str, ""),
61
+ "headings.hideTitle": (bool, False),
62
+ "headings.hideChapter": (bool, False),
63
+ "headings.hideUnnumbered": (bool, False),
64
+ "headings.hideScene": (bool, False),
65
+ "headings.hideAltScene": (bool, False),
66
+ "headings.hideSection": (bool, True),
67
+ "headings.centerTitle": (bool, True),
68
+ "headings.centerChapter": (bool, False),
69
+ "headings.centerScene": (bool, False),
70
+ "headings.breakTitle": (bool, True),
71
+ "headings.breakChapter": (bool, True),
72
+ "headings.breakScene": (bool, False),
73
+ "text.includeSynopsis": (bool, False),
74
+ "text.includeComments": (bool, False),
75
+ "text.includeKeywords": (bool, False),
76
+ "text.includeBodyText": (bool, True),
77
+ "text.ignoredKeywords": (str, ""),
78
+ "text.addNoteHeadings": (bool, True),
79
+ "format.textFont": (str, CONFIG.textFont),
80
+ "format.textSize": (int, 12),
81
+ "format.lineHeight": (float, 1.15, 0.75, 3.0),
82
+ "format.justifyText": (bool, False),
83
+ "format.stripUnicode": (bool, False),
84
+ "format.replaceTabs": (bool, False),
85
+ "format.pageUnit": (str, "cm"),
86
+ "format.pageSize": (str, "A4"),
87
+ "format.pageWidth": (float, 21.0),
88
+ "format.pageHeight": (float, 29.7),
89
+ "format.topMargin": (float, 2.0),
90
+ "format.bottomMargin": (float, 2.0),
91
+ "format.leftMargin": (float, 2.0),
92
+ "format.rightMargin": (float, 2.0),
93
+ "odt.addColours": (bool, True),
94
+ "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
95
+ "odt.pageCountOffset": (int, 0),
96
+ "odt.firstLineIndent": (bool, False),
97
+ "md.preserveBreaks": (bool, True),
98
+ "html.addStyles": (bool, True),
99
+ "html.preserveTabs": (bool, False),
85
100
  }
86
101
 
87
102
  SETTINGS_LABELS = {
@@ -91,19 +106,19 @@ SETTINGS_LABELS = {
91
106
  "filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
92
107
 
93
108
  "headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
94
- "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Title Headings"),
95
- "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Headings"),
96
- "headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Headings"),
97
- "headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Headings"),
98
- "headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Headings"),
99
- "headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene Headings"),
100
- "headings.hideSection": QT_TRANSLATE_NOOP("Builds", "Hide Section Headings"),
109
+ "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
110
+ "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
111
+ "headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
112
+ "headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
113
+ "headings.fmtAltScene": QT_TRANSLATE_NOOP("Builds", "Alt. Scene Format"),
114
+ "headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
101
115
 
102
116
  "text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
103
117
  "text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
104
118
  "text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
105
119
  "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
106
120
  "text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
121
+ "text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
107
122
  "text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
108
123
  "text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
109
124
 
@@ -129,9 +144,14 @@ SETTINGS_LABELS = {
129
144
  "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
130
145
  "odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
131
146
  "odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
147
+ "odt.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
148
+
149
+ "md": QT_TRANSLATE_NOOP("Builds", "Markdown (.md)"),
150
+ "md.preserveBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
132
151
 
133
152
  "html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
134
153
  "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
154
+ "html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
135
155
  }
136
156
 
137
157
 
@@ -158,7 +178,7 @@ class BuildSettings:
158
178
  def __init__(self) -> None:
159
179
  self._name = ""
160
180
  self._uuid = str(uuid.uuid4())
161
- self._path = Path.home()
181
+ self._path = CONFIG.homePath()
162
182
  self._build = ""
163
183
  self._order = 0
164
184
  self._format = nwBuildFmt.ODT
@@ -200,7 +220,7 @@ class BuildSettings:
200
220
  """The last used build path."""
201
221
  if self._path.is_dir():
202
222
  return self._path
203
- return Path.home()
223
+ return CONFIG.homePath()
204
224
 
205
225
  @property
206
226
  def lastBuildName(self) -> str:
@@ -277,7 +297,7 @@ class BuildSettings:
277
297
  if isinstance(path, Path) and path.is_dir():
278
298
  self._path = path
279
299
  else:
280
- self._path = Path.home()
300
+ self._path = CONFIG.homePath()
281
301
  self._changed = True
282
302
  return
283
303
 
@@ -362,7 +382,7 @@ class BuildSettings:
362
382
 
363
383
  postponed = []
364
384
 
365
- def allowRoot(rHandle):
385
+ def allowRoot(rHandle: str | None) -> None:
366
386
  if rHandle in postponed and rHandle in result and rHandle is not None:
367
387
  result[rHandle] = (True, FilterMode.ROOT)
368
388
  postponed.remove(rHandle)
@@ -26,19 +26,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
26
26
  """
27
27
  from __future__ import annotations
28
28
 
29
- import shutil
30
29
  import logging
30
+ import shutil
31
31
 
32
- from pathlib import Path
32
+ from collections.abc import Iterable
33
33
  from functools import partial
34
+ from pathlib import Path
34
35
  from zipfile import ZipFile, is_zipfile
35
- from collections.abc import Iterable
36
36
 
37
- from PyQt5.QtCore import QCoreApplication
37
+ from PyQt5.QtCore import QCoreApplication, QRegularExpression
38
38
 
39
39
  from novelwriter import CONFIG, SHARED
40
40
  from novelwriter.common import isHandle, minmax, simplified
41
- from novelwriter.constants import nwFiles, nwItemClass
41
+ from novelwriter.constants import nwConst, nwFiles, nwItemClass
42
42
  from novelwriter.core.item import NWItem
43
43
  from novelwriter.core.project import NWProject
44
44
  from novelwriter.core.storage import NWStorageCreate
@@ -101,9 +101,7 @@ class DocMerger:
101
101
  if srcItem is None:
102
102
  return False
103
103
 
104
- inDoc = self._project.storage.getDocument(srcHandle)
105
- docText = (inDoc.readDocument() or "").rstrip("\n")
106
-
104
+ docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n")
107
105
  if addComment:
108
106
  docInfo = srcItem.describeMe()
109
107
  docSt, _ = srcItem.getImportStatus(incIcon=False)
@@ -122,9 +120,8 @@ class DocMerger:
122
120
  return False
123
121
 
124
122
  outDoc = self._project.storage.getDocument(self._targetDoc)
125
- docText = (outDoc.readDocument() or "").rstrip("\n")
126
- if docText:
127
- self._targetText.insert(0, docText)
123
+ if text := (outDoc.readDocument() or "").rstrip("\n"):
124
+ self._targetText.insert(0, text)
128
125
 
129
126
  status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
130
127
  if not status:
@@ -292,11 +289,10 @@ class DocDuplicator:
292
289
  newItem.setParent(hMap[newItem.itemParent])
293
290
  self._project.tree.updateItemData(newItem.itemHandle)
294
291
  if newItem.isFileType():
295
- oldDoc = self._project.storage.getDocument(tHandle)
296
292
  newDoc = self._project.storage.getDocument(newItem.itemHandle)
297
293
  if newDoc.fileExists():
298
294
  return
299
- newDoc.writeDocument(oldDoc.readDocument() or "")
295
+ newDoc.writeDocument(self._project.storage.getDocumentText(tHandle))
300
296
  yield newItem.itemHandle, nHandle
301
297
  nHandle = None
302
298
  return
@@ -304,6 +300,94 @@ class DocDuplicator:
304
300
  # END Class DocDuplicator
305
301
 
306
302
 
303
+ class DocSearch:
304
+
305
+ def __init__(self) -> None:
306
+ self._regEx = QRegularExpression()
307
+ self.setCaseSensitive(False)
308
+ self._words = False
309
+ self._escape = True
310
+ return
311
+
312
+ ##
313
+ # Methods
314
+ ##
315
+
316
+ def setCaseSensitive(self, state: bool) -> None:
317
+ """Set the case sensitive search flag."""
318
+ opts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
319
+ if not state:
320
+ opts |= QRegularExpression.PatternOption.CaseInsensitiveOption
321
+ self._regEx.setPatternOptions(opts)
322
+ return
323
+
324
+ def setWholeWords(self, state: bool) -> None:
325
+ """Set the whole words search flag."""
326
+ self._words = state
327
+ return
328
+
329
+ def setUserRegEx(self, state: bool) -> None:
330
+ """Set the escape flag to the opposite state."""
331
+ self._escape = not state
332
+ return
333
+
334
+ def iterSearch(
335
+ self, project: NWProject, search: str
336
+ ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
337
+ """Iteratively search through documents in a project."""
338
+ self._regEx.setPattern(self._buildPattern(search))
339
+ logger.debug("Searching with pattern '%s'", self._regEx.pattern())
340
+ storage = project.storage
341
+ for item in project.tree:
342
+ if item.isFileType():
343
+ results, capped = self.searchText(storage.getDocumentText(item.itemHandle))
344
+ yield item, results, capped
345
+ return
346
+
347
+ def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
348
+ """Search a piece of text for RegEx matches."""
349
+ rxItt = self._regEx.globalMatch(text)
350
+ count = 0
351
+ capped = False
352
+ results = []
353
+ while rxItt.hasNext():
354
+ rxMatch = rxItt.next()
355
+ pos = rxMatch.capturedStart()
356
+ num = rxMatch.capturedLength()
357
+ context = text[pos:pos+100].partition("\n")[0]
358
+ if context:
359
+ results.append((pos, num, context))
360
+ count += 1
361
+ if count >= nwConst.MAX_SEARCH_RESULT:
362
+ capped = True
363
+ break
364
+ return results, capped
365
+
366
+ ##
367
+ # Internal Functions
368
+ ##
369
+
370
+ def _buildPattern(self, search: str) -> str:
371
+ """Build the search pattern string."""
372
+ if self._escape:
373
+ if CONFIG.verQtValue >= 0x050f00:
374
+ search = QRegularExpression.escape(search)
375
+ else:
376
+ # For older Qt versions, we escape manually
377
+ escaped = ""
378
+ for c in search:
379
+ if c.isalnum() or c == "_":
380
+ escaped += c
381
+ else:
382
+ escaped += f"\\{c}"
383
+ search = escaped
384
+ if self._words:
385
+ search = f"(?:^|\\b){search}(?:$|\\b)"
386
+ return search
387
+
388
+ # END Class DocSearch
389
+
390
+
307
391
  class ProjectBuilder:
308
392
  """A class to build a new project from a set of user-defined
309
393
  parameter provided by the New Project Wizard.
@@ -52,7 +52,10 @@ class NWBuildDocument:
52
52
  manuscript, based on a build definition object (BuildSettings).
53
53
  """
54
54
 
55
- __slots__ = ("_project", "_build", "_queue", "_error", "_cache")
55
+ __slots__ = (
56
+ "_project", "_build", "_queue", "_error", "_cache", "_count",
57
+ "_outline", "_preview"
58
+ )
56
59
 
57
60
  def __init__(self, project: NWProject, build: BuildSettings) -> None:
58
61
  self._project = project
@@ -60,6 +63,9 @@ class NWBuildDocument:
60
63
  self._queue = []
61
64
  self._error = None
62
65
  self._cache = None
66
+ self._count = False
67
+ self._outline = False
68
+ self._preview = False
63
69
  return
64
70
 
65
71
  ##
@@ -79,6 +85,29 @@ class NWBuildDocument:
79
85
  """
80
86
  return self._cache
81
87
 
88
+ ##
89
+ # Setters
90
+ ##
91
+
92
+ def setCountEnabled(self, state: bool) -> None:
93
+ """Turn on/off stats for builds."""
94
+ self._count = state
95
+ return
96
+
97
+ def setBuildOutline(self, state: bool) -> None:
98
+ """Turn on/off outline for builds."""
99
+ self._outline = state
100
+ return
101
+
102
+ def setPreviewMode(self, state: bool) -> None:
103
+ """Set the preview mode of the build. This also enables stats
104
+ count and outline mode.
105
+ """
106
+ self._preview = state
107
+ self._outline = state
108
+ self._count = state
109
+ return
110
+
82
111
  ##
83
112
  # Special Methods
84
113
  ##
@@ -153,6 +182,8 @@ class NWBuildDocument:
153
182
  makeObj = ToHtml(self._project)
154
183
  filtered = self._setupBuild(makeObj)
155
184
 
185
+ makeObj.setPreview(self._preview)
186
+ makeObj.setLinkHeadings(self._preview)
156
187
  for i, tHandle in enumerate(self._queue):
157
188
  self._error = None
158
189
  if filtered.get(tHandle, (False, 0))[0]:
@@ -160,7 +191,7 @@ class NWBuildDocument:
160
191
  else:
161
192
  yield i, False
162
193
 
163
- if self._build.getBool("format.replaceTabs"):
194
+ if not (self._build.getBool("html.preserveTabs") or self._preview):
164
195
  makeObj.replaceTabs()
165
196
 
166
197
  self._error = None
@@ -191,6 +222,8 @@ class NWBuildDocument:
191
222
  if self._build.getBool("format.replaceTabs"):
192
223
  makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
193
224
 
225
+ makeObj.setPreserveBreaks(self._build.getBool("md.preserveBreaks"))
226
+
194
227
  for i, tHandle in enumerate(self._queue):
195
228
  self._error = None
196
229
  if filtered.get(tHandle, (False, 0))[0]:
@@ -256,26 +289,52 @@ class NWBuildDocument:
256
289
  fontInfo = QFontInfo(bldFont)
257
290
  textFixed = fontInfo.fixedPitch()
258
291
 
259
- bldObj.setTitleFormat(self._build.getStr("headings.fmtTitle"))
260
- bldObj.setChapterFormat(self._build.getStr("headings.fmtChapter"))
261
- bldObj.setUnNumberedFormat(self._build.getStr("headings.fmtUnnumbered"))
292
+ bldObj.setTitleFormat(
293
+ self._build.getStr("headings.fmtTitle"),
294
+ self._build.getBool("headings.hideTitle")
295
+ )
296
+ bldObj.setChapterFormat(
297
+ self._build.getStr("headings.fmtChapter"),
298
+ self._build.getBool("headings.hideChapter")
299
+ )
300
+ bldObj.setUnNumberedFormat(
301
+ self._build.getStr("headings.fmtUnnumbered"),
302
+ self._build.getBool("headings.hideUnnumbered")
303
+ )
262
304
  bldObj.setSceneFormat(
263
305
  self._build.getStr("headings.fmtScene"),
264
306
  self._build.getBool("headings.hideScene")
265
307
  )
308
+ bldObj.setHardSceneFormat(
309
+ self._build.getStr("headings.fmtAltScene"),
310
+ self._build.getBool("headings.hideAltScene")
311
+ )
266
312
  bldObj.setSectionFormat(
267
313
  self._build.getStr("headings.fmtSection"),
268
314
  self._build.getBool("headings.hideSection")
269
315
  )
316
+ bldObj.setTitleStyle(
317
+ self._build.getBool("headings.centerTitle"),
318
+ self._build.getBool("headings.breakTitle")
319
+ )
320
+ bldObj.setChapterStyle(
321
+ self._build.getBool("headings.centerChapter"),
322
+ self._build.getBool("headings.breakChapter")
323
+ )
324
+ bldObj.setSceneStyle(
325
+ self._build.getBool("headings.centerScene"),
326
+ self._build.getBool("headings.breakScene")
327
+ )
270
328
 
271
329
  bldObj.setFont(fontFamily, textSize, textFixed)
272
330
  bldObj.setJustify(self._build.getBool("format.justifyText"))
273
331
  bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
274
332
 
333
+ bldObj.setBodyText(self._build.getBool("text.includeBodyText"))
275
334
  bldObj.setSynopsis(self._build.getBool("text.includeSynopsis"))
276
335
  bldObj.setComments(self._build.getBool("text.includeComments"))
277
336
  bldObj.setKeywords(self._build.getBool("text.includeKeywords"))
278
- bldObj.setBodyText(self._build.getBool("text.includeBodyText"))
337
+ bldObj.setIgnoredKeywords(self._build.getStr("text.ignoredKeywords"))
279
338
 
280
339
  if isinstance(bldObj, ToHtml):
281
340
  bldObj.setStyles(self._build.getBool("html.addStyles"))
@@ -287,6 +346,7 @@ class NWBuildDocument:
287
346
  bldObj.setHeaderFormat(
288
347
  self._build.getStr("odt.pageHeader"), self._build.getInt("odt.pageCountOffset")
289
348
  )
349
+ bldObj.setFirstLineIndent(self._build.getBool("odt.firstLineIndent"))
290
350
 
291
351
  scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0)
292
352
  pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0))
@@ -314,11 +374,18 @@ class NWBuildDocument:
314
374
  bldObj.addRootHeading(tHandle)
315
375
  if convert:
316
376
  bldObj.doConvert()
377
+ if self._count:
378
+ bldObj.countStats()
379
+ if self._outline:
380
+ bldObj.buildOutline()
317
381
  elif tItem.isFileType():
318
382
  bldObj.setText(tHandle)
319
383
  bldObj.doPreProcessing()
320
384
  bldObj.tokenizeText()
321
- bldObj.doHeaders()
385
+ if self._count:
386
+ bldObj.countStats()
387
+ if self._outline:
388
+ bldObj.buildOutline()
322
389
  if convert:
323
390
  bldObj.doConvert()
324
391
  else:
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
31
31
  from pathlib import Path
32
32
 
33
33
  from novelwriter.enum import nwItemLayout, nwItemClass
34
- from novelwriter.error import formatException
34
+ from novelwriter.error import formatException, logException
35
35
  from novelwriter.common import formatTimeStamp, isHandle
36
36
  from novelwriter.core.item import NWItem
37
37
 
@@ -106,7 +106,28 @@ class NWDocument:
106
106
  return self._item
107
107
 
108
108
  ##
109
- # Class Methods
109
+ # Static Methods
110
+ ##
111
+
112
+ @staticmethod
113
+ def quickReadText(content: Path, tHandle: str) -> str:
114
+ """Return the text of a document in a fast and efficient way."""
115
+ if (path := content / f"{tHandle}.nwd").is_file():
116
+ try:
117
+ with open(path, mode="r", encoding="utf-8") as inFile:
118
+ line = ""
119
+ for _ in range(10):
120
+ if not (line := inFile.readline()).startswith(r"%%~"):
121
+ break
122
+ return line + inFile.read()
123
+ except Exception:
124
+ logger.error("Cannot read document with handle '%s'", tHandle)
125
+ logException()
126
+ return ""
127
+ return ""
128
+
129
+ ##
130
+ # Methods
110
131
  ##
111
132
 
112
133
  def fileExists(self) -> bool:
@@ -155,7 +176,7 @@ class NWDocument:
155
176
  try:
156
177
  with open(docPath, mode="r", encoding="utf-8") as inFile:
157
178
  # Check the first <= 10 lines for metadata
158
- for i in range(10):
179
+ for _ in range(10):
159
180
  line = inFile.readline()
160
181
  if line.startswith(r"%%~"):
161
182
  self._parseMeta(line)