novelWriter 2.3rc1__py3-none-any.whl → 2.4b1__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 (100) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +99 -85
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4b1.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +5 -5
  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/project_nl_NL.json +11 -0
  15. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  16. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  18. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  21. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  24. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  25. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  26. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  29. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  32. novelwriter/assets/manual.pdf +0 -0
  33. novelwriter/assets/sample.zip +0 -0
  34. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  35. novelwriter/assets/syntax/default_dark.conf +1 -0
  36. novelwriter/assets/syntax/default_light.conf +1 -0
  37. novelwriter/assets/syntax/grey_dark.conf +1 -0
  38. novelwriter/assets/syntax/grey_light.conf +1 -0
  39. novelwriter/assets/syntax/light_owl.conf +1 -0
  40. novelwriter/assets/syntax/night_owl.conf +1 -0
  41. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  42. novelwriter/assets/syntax/solarized_light.conf +1 -0
  43. novelwriter/assets/syntax/tango.conf +23 -0
  44. novelwriter/assets/syntax/tomorrow.conf +1 -0
  45. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  46. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  47. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  49. novelwriter/assets/text/credits_en.htm +25 -23
  50. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  51. novelwriter/common.py +1 -1
  52. novelwriter/config.py +35 -12
  53. novelwriter/constants.py +5 -6
  54. novelwriter/core/buildsettings.py +60 -40
  55. novelwriter/core/coretools.py +98 -13
  56. novelwriter/core/docbuild.py +74 -7
  57. novelwriter/core/document.py +24 -3
  58. novelwriter/core/index.py +31 -112
  59. novelwriter/core/project.py +11 -15
  60. novelwriter/core/projectxml.py +2 -1
  61. novelwriter/core/sessions.py +2 -2
  62. novelwriter/core/status.py +4 -4
  63. novelwriter/core/storage.py +16 -6
  64. novelwriter/core/tohtml.py +22 -25
  65. novelwriter/core/tokenizer.py +416 -236
  66. novelwriter/core/tomd.py +17 -8
  67. novelwriter/core/toodt.py +65 -7
  68. novelwriter/core/tree.py +8 -8
  69. novelwriter/dialogs/about.py +2 -2
  70. novelwriter/dialogs/docsplit.py +7 -8
  71. novelwriter/dialogs/preferences.py +3 -6
  72. novelwriter/dialogs/wordlist.py +1 -1
  73. novelwriter/enum.py +17 -14
  74. novelwriter/extensions/configlayout.py +22 -0
  75. novelwriter/extensions/modified.py +20 -2
  76. novelwriter/extensions/versioninfo.py +1 -1
  77. novelwriter/gui/doceditor.py +257 -279
  78. novelwriter/gui/dochighlight.py +29 -25
  79. novelwriter/gui/docviewer.py +139 -148
  80. novelwriter/gui/docviewerpanel.py +4 -24
  81. novelwriter/gui/editordocument.py +12 -1
  82. novelwriter/gui/itemdetails.py +6 -6
  83. novelwriter/gui/mainmenu.py +37 -17
  84. novelwriter/gui/noveltree.py +11 -19
  85. novelwriter/gui/outline.py +43 -20
  86. novelwriter/gui/projtree.py +88 -88
  87. novelwriter/gui/search.py +316 -0
  88. novelwriter/gui/sidebar.py +25 -30
  89. novelwriter/gui/theme.py +68 -8
  90. novelwriter/guimain.py +183 -178
  91. novelwriter/shared.py +26 -1
  92. novelwriter/text/__init__.py +3 -0
  93. novelwriter/text/counting.py +137 -0
  94. novelwriter/tools/manuscript.py +344 -55
  95. novelwriter/tools/manussettings.py +214 -71
  96. novelwriter/tools/noveldetails.py +1 -1
  97. novelwriter/tools/welcome.py +8 -9
  98. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  99. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
  100. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
@@ -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,95 @@ 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
+
341
+ num = len(search)
342
+ storage = project.storage
343
+ for item in project.tree:
344
+ if item.isFileType():
345
+ text = storage.getDocumentText(item.itemHandle)
346
+ rxItt = self._regEx.globalMatch(text)
347
+ count = 0
348
+ capped = False
349
+ results = []
350
+ while rxItt.hasNext():
351
+ rxMatch = rxItt.next()
352
+ pos = rxMatch.capturedStart()
353
+ num = rxMatch.capturedLength()
354
+ context = text[pos:pos+100].partition("\n")[0]
355
+ if context:
356
+ results.append((pos, num, context))
357
+ count += 1
358
+ if count >= nwConst.MAX_SEARCH_RESULT:
359
+ capped = True
360
+ break
361
+
362
+ yield item, results, capped
363
+
364
+ return
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 = search if search.startswith("\\b") else f"\\b{search}"
386
+ search = search if search.endswith("\\b") else f"{search}\\b"
387
+ return search
388
+
389
+ # END Class DocSearch
390
+
391
+
307
392
  class ProjectBuilder:
308
393
  """A class to build a new project from a set of user-defined
309
394
  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.fmtHardScene"),
310
+ self._build.getBool("headings.hideHardScene")
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)
novelwriter/core/index.py CHANGED
@@ -3,7 +3,6 @@ novelWriter – Project Index
3
3
  ===========================
4
4
 
5
5
  File History:
6
- Created: 2019-04-22 [0.0.1] countWords
7
6
  Created: 2019-05-27 [0.1.4] NWIndex
8
7
  Created: 2022-05-28 [2.0rc1] IndexItem
9
8
  Created: 2022-05-28 [2.0rc1] IndexHeading
@@ -34,13 +33,14 @@ import logging
34
33
  from time import time
35
34
  from typing import TYPE_CHECKING
36
35
  from pathlib import Path
37
- from collections.abc import ItemsView, Iterable, Iterator
36
+ from collections.abc import ItemsView, Iterable
38
37
 
39
38
  from novelwriter import SHARED
40
39
  from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
41
40
  from novelwriter.error import logException
42
41
  from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
43
- from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
42
+ from novelwriter.constants import nwFiles, nwKeyWords, nwHeaders
43
+ from novelwriter.text.counting import standardCounter
44
44
 
45
45
  if TYPE_CHECKING: # pragma: no cover
46
46
  from novelwriter.core.item import NWItem
@@ -122,9 +122,8 @@ class NWIndex:
122
122
  self.clearIndex()
123
123
  for nwItem in self._project.tree:
124
124
  if nwItem.isFileType():
125
- tHandle = nwItem.itemHandle
126
- doc = self._project.storage.getDocument(tHandle)
127
- self.scanText(tHandle, doc.readDocument() or "", blockSignal=True)
125
+ text = self._project.storage.getDocumentText(nwItem.itemHandle)
126
+ self.scanText(nwItem.itemHandle, text, blockSignal=True)
128
127
  self._indexBroken = False
129
128
  SHARED.indexSignalProxy({"event": "buildIndex"})
130
129
  return
@@ -142,17 +141,15 @@ class NWIndex:
142
141
  })
143
142
  return
144
143
 
145
- def reIndexHandle(self, tHandle: str | None) -> bool:
144
+ def reIndexHandle(self, tHandle: str | None) -> None:
146
145
  """Put a file back into the index. This is used when files are
147
146
  moved from the archive or trash folders back into the active
148
147
  project.
149
148
  """
150
149
  if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
151
150
  logger.debug("Re-indexing item '%s'", tHandle)
152
- doc = self._project.storage.getDocument(tHandle)
153
- self.scanText(tHandle, doc.readDocument() or "")
154
- return True
155
- return False
151
+ self.scanText(tHandle, self._project.storage.getDocumentText(tHandle))
152
+ return
156
153
 
157
154
  def indexChangedSince(self, checkTime: int | float) -> bool:
158
155
  """Check if the index has changed since a given time."""
@@ -266,7 +263,7 @@ class NWIndex:
266
263
  self._itemIndex.add(tHandle, tItem)
267
264
 
268
265
  # Run word counter for the whole text
269
- cC, wC, pC = countWords(text)
266
+ cC, wC, pC = standardCounter(text)
270
267
  tItem.setCharCount(cC)
271
268
  tItem.setWordCount(wC)
272
269
  tItem.setParaCount(pC)
@@ -307,7 +304,7 @@ class NWIndex:
307
304
  nTitle = 0 # Line Number of the previous title
308
305
  cTitle = TT_NONE # Tag of the current title
309
306
  pTitle = TT_NONE # Tag of the previous title
310
- canSetHeader = True # First header has not yet been set
307
+ canSetHead = True # First heading has not yet been set
311
308
 
312
309
  lines = text.splitlines()
313
310
  for n, line in enumerate(lines, start=1):
@@ -320,9 +317,9 @@ class NWIndex:
320
317
  if hDepth == "H0":
321
318
  continue
322
319
 
323
- if canSetHeader:
320
+ if canSetHead:
324
321
  nwItem.setMainHeading(hDepth)
325
- canSetHeader = False
322
+ canSetHead = False
326
323
 
327
324
  cTitle = self._itemIndex.addItemHeading(tHandle, n, hDepth, hText)
328
325
  if cTitle != TT_NONE:
@@ -383,7 +380,7 @@ class NWIndex:
383
380
  return
384
381
 
385
382
  def _splitHeading(self, line: str) -> tuple[str, str]:
386
- """Split a heading into its header level and text value."""
383
+ """Split a heading into its heading level and text value."""
387
384
  if line.startswith("# "):
388
385
  return "H1", line[2:].strip()
389
386
  elif line.startswith("## "):
@@ -396,11 +393,13 @@ class NWIndex:
396
393
  return "H1", line[3:].strip()
397
394
  elif line.startswith("##! "):
398
395
  return "H2", line[4:].strip()
396
+ elif line.startswith("###! "):
397
+ return "H3", line[5:].strip()
399
398
  return "H0", ""
400
399
 
401
400
  def _indexWordCounts(self, tHandle: str, text: str, sTitle: str) -> None:
402
401
  """Count text stats and save the counts to the index."""
403
- cC, wC, pC = countWords(text)
402
+ cC, wC, pC = standardCounter(text)
404
403
  self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
405
404
  return
406
405
 
@@ -515,16 +514,21 @@ class NWIndex:
515
514
  """Get the index data for a given item."""
516
515
  return self._itemIndex[tHandle]
517
516
 
518
- def getItemHeader(self, tHandle: str, sTitle: str) -> IndexHeading | None:
519
- """Get the header entry for a specific item and heading."""
520
- tItem = self._itemIndex[tHandle]
521
- if isinstance(tItem, IndexItem):
517
+ def getItemHeading(self, tHandle: str, sTitle: str) -> IndexHeading | None:
518
+ """Get the heading entry for a specific item and heading."""
519
+ if tItem := self._itemIndex[tHandle]:
522
520
  return tItem[sTitle]
523
521
  return None
524
522
 
523
+ def iterItemHeadings(self, tHandle: str) -> Iterable[tuple[str, IndexHeading]]:
524
+ """Get all headings for a specific item."""
525
+ if tItem := self._itemIndex[tHandle]:
526
+ yield from tItem.items()
527
+ return []
528
+
525
529
  def novelStructure(
526
530
  self, rootHandle: str | None = None, activeOnly: bool = True
527
- ) -> Iterator[tuple[str, str, str, IndexHeading]]:
531
+ ) -> Iterable[tuple[str, str, str, IndexHeading]]:
528
532
  """Iterate over all titles in the novel, in the correct order as
529
533
  they appear in the tree view and in the respective document
530
534
  files, but skipping all note files.
@@ -666,7 +670,7 @@ class NWIndex:
666
670
 
667
671
  def getTagsData(
668
672
  self, activeOnly: bool = True
669
- ) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
673
+ ) -> Iterable[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
670
674
  """Return all known tags."""
671
675
  for tag, data in self._tagsIndex.items():
672
676
  iItem = self._itemIndex[data.get("handle")]
@@ -825,7 +829,7 @@ class ItemIndex:
825
829
  class around a single storage dictionary with a set of utility
826
830
  functions for setting and accessing the index data. Each indexed
827
831
  item is stored in an IndexItem object, which again holds an
828
- IndexHeading object for each header of the text.
832
+ IndexHeading object for each heading of the text.
829
833
  """
830
834
 
831
835
  __slots__ = ("_project", "_items")
@@ -898,13 +902,11 @@ class ItemIndex:
898
902
 
899
903
  if rHandle is None:
900
904
  for sTitle in self._items[tHandle].headings():
901
- hItem = self._items[tHandle][sTitle]
902
- if hItem:
905
+ if hItem := self._items[tHandle][sTitle]:
903
906
  yield tHandle, sTitle, hItem
904
907
  elif tItem.itemRoot == rHandle:
905
908
  for sTitle in self._items[tHandle].headings():
906
- hItem = self._items[tHandle][sTitle]
907
- if hItem:
909
+ if hItem := self._items[tHandle][sTitle]:
908
910
  yield tHandle, sTitle, hItem
909
911
 
910
912
  return
@@ -1200,7 +1202,7 @@ class IndexHeading:
1200
1202
  ##
1201
1203
 
1202
1204
  def setLevel(self, level: str) -> None:
1203
- """Set the level of the header if it's a valid value."""
1205
+ """Set the level of the heading if it's a valid value."""
1204
1206
  if level in nwHeaders.H_VALID:
1205
1207
  self._level = level
1206
1208
  return
@@ -1315,86 +1317,3 @@ def processComment(text: str) -> tuple[nwComment, str, int]:
1315
1317
  if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
1316
1318
  return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
1317
1319
  return nwComment.PLAIN, check, 0
1318
-
1319
-
1320
- def countWords(text: str) -> tuple[int, int, int]:
1321
- """Count words in a piece of text, skipping special syntax and
1322
- comments.
1323
- """
1324
- charCount = 0
1325
- wordCount = 0
1326
- paraCount = 0
1327
- prevEmpty = True
1328
-
1329
- if not isinstance(text, str):
1330
- return charCount, wordCount, paraCount
1331
-
1332
- # We need to treat dashes as word separators for counting words.
1333
- # The check+replace approach is much faster than direct replace for
1334
- # large texts, and a bit slower for small texts, but in the latter
1335
- # case it doesn't really matter.
1336
- if nwUnicode.U_ENDASH in text:
1337
- text = text.replace(nwUnicode.U_ENDASH, " ")
1338
- if nwUnicode.U_EMDASH in text:
1339
- text = text.replace(nwUnicode.U_EMDASH, " ")
1340
-
1341
- # Strip shortcodes
1342
- if "[" in text:
1343
- text = nwRegEx.RX_SC.sub("", text)
1344
-
1345
- for line in text.splitlines():
1346
-
1347
- countPara = True
1348
-
1349
- if not line:
1350
- prevEmpty = True
1351
- continue
1352
-
1353
- if line[0] == "@" or line[0] == "%":
1354
- continue
1355
-
1356
- if line[0] == "[":
1357
- check = line.lower()
1358
- if check.startswith(("[newpage]", "[new page]", "[vspace]")):
1359
- continue
1360
- elif check.startswith("[vspace:") and line.endswith("]"):
1361
- continue
1362
-
1363
- elif line[0] == "#":
1364
- if line[:5] == "#### ":
1365
- line = line[5:]
1366
- countPara = False
1367
- elif line[:4] == "### ":
1368
- line = line[4:]
1369
- countPara = False
1370
- elif line[:3] == "## ":
1371
- line = line[3:]
1372
- countPara = False
1373
- elif line[:2] == "# ":
1374
- line = line[2:]
1375
- countPara = False
1376
- elif line[:3] == "#! ":
1377
- line = line[3:]
1378
- countPara = False
1379
- elif line[:4] == "##! ":
1380
- line = line[4:]
1381
- countPara = False
1382
-
1383
- elif line[0] == ">" or line[-1] == "<":
1384
- if line[:2] == ">>":
1385
- line = line[2:].lstrip(" ")
1386
- elif line[:1] == ">":
1387
- line = line[1:].lstrip(" ")
1388
- if line[-2:] == "<<":
1389
- line = line[:-2].rstrip(" ")
1390
- elif line[-1:] == "<":
1391
- line = line[:-1].rstrip(" ")
1392
-
1393
- wordCount += len(line.split())
1394
- charCount += len(line)
1395
- if countPara and prevEmpty:
1396
- paraCount += 1
1397
-
1398
- prevEmpty = not countPara
1399
-
1400
- return charCount, wordCount, paraCount