novelWriter 2.3.1__py3-none-any.whl → 2.4rc1__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.
- {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/METADATA +5 -6
- {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/RECORD +102 -95
- novelwriter/__init__.py +7 -7
- novelwriter/assets/icons/none.svg +4 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +25 -23
- novelwriter/common.py +7 -2
- novelwriter/config.py +43 -16
- novelwriter/constants.py +5 -6
- novelwriter/core/buildsettings.py +60 -40
- novelwriter/core/coretools.py +97 -13
- novelwriter/core/docbuild.py +74 -7
- novelwriter/core/document.py +24 -3
- novelwriter/core/index.py +31 -112
- novelwriter/core/project.py +10 -15
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/status.py +6 -5
- novelwriter/core/storage.py +8 -2
- novelwriter/core/tohtml.py +22 -25
- novelwriter/core/tokenizer.py +416 -232
- novelwriter/core/tomd.py +17 -8
- novelwriter/core/toodt.py +385 -350
- novelwriter/core/tree.py +8 -8
- novelwriter/dialogs/about.py +9 -11
- novelwriter/dialogs/docmerge.py +17 -14
- novelwriter/dialogs/docsplit.py +20 -19
- novelwriter/dialogs/editlabel.py +5 -4
- novelwriter/dialogs/preferences.py +31 -39
- novelwriter/dialogs/projectsettings.py +29 -26
- novelwriter/dialogs/quotes.py +10 -9
- novelwriter/dialogs/wordlist.py +15 -12
- novelwriter/enum.py +17 -14
- novelwriter/error.py +13 -11
- novelwriter/extensions/circularprogress.py +12 -8
- novelwriter/extensions/configlayout.py +1 -3
- novelwriter/extensions/modified.py +51 -2
- novelwriter/extensions/pagedsidebar.py +16 -14
- novelwriter/extensions/simpleprogress.py +3 -1
- novelwriter/extensions/statusled.py +3 -1
- novelwriter/extensions/switch.py +10 -9
- novelwriter/extensions/switchbox.py +14 -13
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/gui/doceditor.py +413 -478
- novelwriter/gui/dochighlight.py +33 -29
- novelwriter/gui/docviewer.py +162 -175
- novelwriter/gui/docviewerpanel.py +20 -37
- novelwriter/gui/editordocument.py +15 -4
- novelwriter/gui/itemdetails.py +51 -54
- novelwriter/gui/mainmenu.py +37 -16
- novelwriter/gui/noveltree.py +30 -36
- novelwriter/gui/outline.py +114 -92
- novelwriter/gui/projtree.py +60 -66
- novelwriter/gui/search.py +362 -0
- novelwriter/gui/sidebar.py +36 -45
- novelwriter/gui/statusbar.py +14 -14
- novelwriter/gui/theme.py +93 -28
- novelwriter/guimain.py +207 -200
- novelwriter/shared.py +31 -6
- novelwriter/text/counting.py +137 -0
- novelwriter/tools/dictionaries.py +13 -12
- novelwriter/tools/lipsum.py +20 -17
- novelwriter/tools/manusbuild.py +35 -27
- novelwriter/tools/manuscript.py +374 -90
- novelwriter/tools/manussettings.py +261 -124
- novelwriter/tools/noveldetails.py +20 -18
- novelwriter/tools/welcome.py +48 -44
- novelwriter/tools/writingstats.py +61 -55
- novelwriter/types.py +90 -0
- novelwriter/core/__init__.py +0 -3
- novelwriter/dialogs/__init__.py +0 -3
- novelwriter/extensions/__init__.py +0 -3
- novelwriter/gui/__init__.py +0 -3
- novelwriter/tools/__init__.py +0 -3
- {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/WHEEL +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/top_level.txt +0 -0
novelwriter/core/coretools.py
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
126
|
-
|
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(
|
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.
|
novelwriter/core/docbuild.py
CHANGED
@@ -52,7 +52,10 @@ class NWBuildDocument:
|
|
52
52
|
manuscript, based on a build definition object (BuildSettings).
|
53
53
|
"""
|
54
54
|
|
55
|
-
__slots__ = (
|
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("
|
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(
|
260
|
-
|
261
|
-
|
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.
|
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
|
-
|
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:
|
novelwriter/core/document.py
CHANGED
@@ -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
|
-
#
|
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
|
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
|
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,
|
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
|
-
|
126
|
-
|
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) ->
|
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
|
-
|
153
|
-
|
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 =
|
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
|
-
|
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
|
320
|
+
if canSetHead:
|
324
321
|
nwItem.setMainHeading(hDepth)
|
325
|
-
|
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
|
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 =
|
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
|
519
|
-
"""Get the
|
520
|
-
tItem
|
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
|
-
) ->
|
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
|
-
) ->
|
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
|
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
|
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
|
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
|
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
|