novelWriter 2.1.1__py3-none-any.whl → 2.2rc1__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.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
- novelwriter/__init__.py +6 -24
- novelwriter/assets/i18n/project_de_DE.json +10 -0
- novelwriter/assets/i18n/project_en_GB.json +11 -0
- novelwriter/assets/i18n/project_en_US.json +10 -0
- novelwriter/assets/i18n/project_ja_JP.json +11 -1
- novelwriter/assets/i18n/project_nb_NO.json +10 -0
- novelwriter/assets/i18n/project_nn_NO.json +10 -0
- novelwriter/assets/icons/novelwriter.ico +0 -0
- novelwriter/assets/icons/novelwriter.svg +8 -183
- novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
- novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +17 -2
- novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
- novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
- novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
- novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
- novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
- 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 +7 -0
- novelwriter/assets/text/release_notes.htm +7 -37
- novelwriter/common.py +22 -1
- novelwriter/config.py +27 -42
- novelwriter/constants.py +45 -7
- novelwriter/core/buildsettings.py +40 -24
- novelwriter/core/coretools.py +8 -1
- novelwriter/core/docbuild.py +2 -6
- novelwriter/core/index.py +264 -175
- novelwriter/core/options.py +8 -3
- novelwriter/core/project.py +2 -2
- novelwriter/core/projectdata.py +3 -3
- novelwriter/core/tohtml.py +60 -59
- novelwriter/core/tokenizer.py +110 -70
- novelwriter/core/tomd.py +51 -38
- novelwriter/core/toodt.py +184 -147
- novelwriter/dialogs/preferences.py +75 -106
- novelwriter/dialogs/projsettings.py +101 -110
- novelwriter/dialogs/updates.py +25 -14
- novelwriter/enum.py +28 -3
- novelwriter/extensions/novelselector.py +1 -1
- novelwriter/gui/doceditor.py +1345 -1235
- novelwriter/gui/dochighlight.py +98 -62
- novelwriter/gui/docviewer.py +151 -340
- novelwriter/gui/docviewerpanel.py +457 -0
- novelwriter/gui/editordocument.py +126 -0
- novelwriter/gui/mainmenu.py +350 -300
- novelwriter/gui/noveltree.py +101 -125
- novelwriter/gui/outline.py +154 -171
- novelwriter/gui/projtree.py +480 -380
- novelwriter/gui/sidebar.py +106 -75
- novelwriter/gui/statusbar.py +1 -1
- novelwriter/gui/theme.py +114 -75
- novelwriter/guimain.py +353 -254
- novelwriter/shared.py +36 -3
- novelwriter/tools/dictionaries.py +268 -0
- novelwriter/tools/manusbuild.py +17 -6
- novelwriter/tools/manuscript.py +11 -3
- novelwriter/tools/manussettings.py +0 -14
- novelwriter/tools/projwizard.py +16 -2
- novelwriter/tools/writingstats.py +1 -1
- novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
novelwriter/core/index.py
CHANGED
@@ -35,10 +35,11 @@ from time import time
|
|
35
35
|
from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
|
36
36
|
from pathlib import Path
|
37
37
|
|
38
|
-
from novelwriter
|
38
|
+
from novelwriter import SHARED
|
39
|
+
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
|
39
40
|
from novelwriter.error import logException
|
40
41
|
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
41
|
-
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
|
42
|
+
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
|
42
43
|
|
43
44
|
if TYPE_CHECKING: # pragma: no cover
|
44
45
|
from novelwriter.core.item import NWItem
|
@@ -112,6 +113,7 @@ class NWIndex:
|
|
112
113
|
self._itemIndex.clear()
|
113
114
|
self._indexChange = 0.0
|
114
115
|
self._rootChange = {}
|
116
|
+
SHARED.indexSignalProxy({"event": "clearIndex"})
|
115
117
|
return
|
116
118
|
|
117
119
|
def rebuildIndex(self) -> None:
|
@@ -121,16 +123,22 @@ class NWIndex:
|
|
121
123
|
if nwItem.isFileType():
|
122
124
|
tHandle = nwItem.itemHandle
|
123
125
|
theDoc = self._project.storage.getDocument(tHandle)
|
124
|
-
self.scanText(tHandle, theDoc.readDocument() or "")
|
126
|
+
self.scanText(tHandle, theDoc.readDocument() or "", blockSignal=True)
|
125
127
|
self._indexBroken = False
|
128
|
+
SHARED.indexSignalProxy({"event": "buildIndex"})
|
126
129
|
return
|
127
130
|
|
128
131
|
def deleteHandle(self, tHandle: str) -> None:
|
129
132
|
"""Delete all entries of a given document handle."""
|
130
133
|
logger.debug("Removing item '%s' from the index", tHandle)
|
131
|
-
|
134
|
+
delTags = self._itemIndex.allItemTags(tHandle)
|
135
|
+
for tTag in delTags:
|
132
136
|
del self._tagsIndex[tTag]
|
133
137
|
del self._itemIndex[tHandle]
|
138
|
+
SHARED.indexSignalProxy({
|
139
|
+
"event": "updateTags",
|
140
|
+
"deleted": delTags,
|
141
|
+
})
|
134
142
|
return
|
135
143
|
|
136
144
|
def reIndexHandle(self, tHandle: str | None) -> bool:
|
@@ -138,24 +146,24 @@ class NWIndex:
|
|
138
146
|
moved from the archive or trash folders back into the active
|
139
147
|
project.
|
140
148
|
"""
|
141
|
-
if tHandle
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
return True
|
149
|
+
if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
|
150
|
+
logger.debug("Re-indexing item '%s'", tHandle)
|
151
|
+
theDoc = self._project.storage.getDocument(tHandle)
|
152
|
+
self.scanText(tHandle, theDoc.readDocument() or "")
|
153
|
+
return True
|
154
|
+
return False
|
149
155
|
|
150
156
|
def indexChangedSince(self, checkTime: int | float) -> bool:
|
151
157
|
"""Check if the index has changed since a given time."""
|
152
158
|
return self._indexChange > float(checkTime)
|
153
159
|
|
154
|
-
def rootChangedSince(self, rootHandle: str, checkTime: int | float) -> bool:
|
160
|
+
def rootChangedSince(self, rootHandle: str | None, checkTime: int | float) -> bool:
|
155
161
|
"""Check if the index has changed since a given time for a
|
156
162
|
given root item.
|
157
163
|
"""
|
158
|
-
|
164
|
+
if isinstance(rootHandle, str):
|
165
|
+
return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
|
166
|
+
return False
|
159
167
|
|
160
168
|
##
|
161
169
|
# Load and Save Index to/from File
|
@@ -167,15 +175,13 @@ class NWIndex:
|
|
167
175
|
if not isinstance(indexFile, Path):
|
168
176
|
return False
|
169
177
|
|
170
|
-
theData = {}
|
171
178
|
tStart = time()
|
172
|
-
|
173
179
|
self._indexBroken = False
|
174
180
|
if indexFile.exists():
|
175
181
|
logger.debug("Loading index file")
|
176
182
|
try:
|
177
183
|
with open(indexFile, mode="r", encoding="utf-8") as inFile:
|
178
|
-
|
184
|
+
data = json.load(inFile)
|
179
185
|
except Exception:
|
180
186
|
logger.error("Failed to load index file")
|
181
187
|
logException()
|
@@ -183,8 +189,8 @@ class NWIndex:
|
|
183
189
|
return False
|
184
190
|
|
185
191
|
try:
|
186
|
-
self._tagsIndex.unpackData(
|
187
|
-
self._itemIndex.unpackData(
|
192
|
+
self._tagsIndex.unpackData(data["novelWriter.tagsIndex"])
|
193
|
+
self._itemIndex.unpackData(data["novelWriter.itemIndex"])
|
188
194
|
except Exception:
|
189
195
|
logger.error("The index content is invalid")
|
190
196
|
logException()
|
@@ -200,6 +206,7 @@ class NWIndex:
|
|
200
206
|
self.reIndexHandle(fHandle)
|
201
207
|
|
202
208
|
self._indexChange = time()
|
209
|
+
SHARED.indexSignalProxy({"event": "buildIndex"})
|
203
210
|
|
204
211
|
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
|
205
212
|
|
@@ -238,50 +245,55 @@ class NWIndex:
|
|
238
245
|
# Index Building
|
239
246
|
##
|
240
247
|
|
241
|
-
def scanText(self, tHandle: str,
|
248
|
+
def scanText(self, tHandle: str, text: str, blockSignal: bool = False) -> bool:
|
242
249
|
"""Scan a piece of text associated with a handle. This will
|
243
250
|
update the indices accordingly. This function takes the handle
|
244
251
|
and text as separate inputs as we want to primarily scan the
|
245
252
|
files before we save them, in which case we already have the
|
246
253
|
text.
|
247
254
|
"""
|
248
|
-
|
249
|
-
if
|
255
|
+
tItem = self._project.tree[tHandle]
|
256
|
+
if tItem is None:
|
250
257
|
logger.info("Not indexing unknown item '%s'", tHandle)
|
251
258
|
return False
|
252
|
-
if not
|
259
|
+
if not tItem.isFileType():
|
253
260
|
logger.info("Not indexing non-file item '%s'", tHandle)
|
254
261
|
return False
|
255
262
|
|
256
263
|
# Keep a record of existing tags, and create a new item entry
|
257
264
|
itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False)
|
258
|
-
self._itemIndex.add(tHandle,
|
265
|
+
self._itemIndex.add(tHandle, tItem)
|
259
266
|
|
260
267
|
# Run word counter for the whole text
|
261
|
-
cC, wC, pC = countWords(
|
262
|
-
|
263
|
-
|
264
|
-
|
268
|
+
cC, wC, pC = countWords(text)
|
269
|
+
tItem.setCharCount(cC)
|
270
|
+
tItem.setWordCount(wC)
|
271
|
+
tItem.setParaCount(pC)
|
265
272
|
|
266
273
|
# If the file's meta data is missing, or the file is out of the
|
267
274
|
# main project, we don't index the content
|
268
|
-
if
|
275
|
+
if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
269
276
|
logger.info("Not indexing no-layout item '%s'", tHandle)
|
270
277
|
return False
|
271
|
-
if
|
278
|
+
if tItem.itemParent is None:
|
272
279
|
logger.info("Not indexing orphaned item '%s'", tHandle)
|
273
280
|
return False
|
274
281
|
|
275
282
|
logger.debug("Indexing item with handle '%s'", tHandle)
|
276
|
-
if
|
277
|
-
self._scanInactive(
|
283
|
+
if tItem.isInactiveClass():
|
284
|
+
self._scanInactive(tItem, text)
|
278
285
|
else:
|
279
|
-
self._scanActive(tHandle,
|
286
|
+
self._scanActive(tHandle, tItem, text, itemTags)
|
280
287
|
|
281
288
|
# Update timestamps for index changes
|
282
289
|
nowTime = time()
|
283
290
|
self._indexChange = nowTime
|
284
|
-
self._rootChange[
|
291
|
+
self._rootChange[tItem.itemRoot] = nowTime
|
292
|
+
if not blockSignal:
|
293
|
+
SHARED.indexSignalProxy({
|
294
|
+
"event": "scanText",
|
295
|
+
"handle": tHandle,
|
296
|
+
})
|
285
297
|
|
286
298
|
return True
|
287
299
|
|
@@ -289,21 +301,21 @@ class NWIndex:
|
|
289
301
|
# Internal Indexer Helpers
|
290
302
|
##
|
291
303
|
|
292
|
-
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict) -> None:
|
304
|
+
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None:
|
293
305
|
"""Scan an active document for meta data."""
|
294
306
|
nTitle = 0 # Line Number of the previous title
|
295
307
|
cTitle = TT_NONE # Tag of the current title
|
296
308
|
pTitle = TT_NONE # Tag of the previous title
|
297
309
|
canSetHeader = True # First header has not yet been set
|
298
310
|
|
299
|
-
|
300
|
-
for
|
311
|
+
lines = text.splitlines()
|
312
|
+
for n, line in enumerate(lines, start=1):
|
301
313
|
|
302
|
-
if
|
314
|
+
if line.strip() == "":
|
303
315
|
continue
|
304
316
|
|
305
|
-
if
|
306
|
-
hDepth, hText = self._splitHeading(
|
317
|
+
if line.startswith("#"):
|
318
|
+
hDepth, hText = self._splitHeading(line)
|
307
319
|
if hDepth == "H0":
|
308
320
|
continue
|
309
321
|
|
@@ -311,33 +323,28 @@ class NWIndex:
|
|
311
323
|
nwItem.setMainHeading(hDepth)
|
312
324
|
canSetHeader = False
|
313
325
|
|
314
|
-
cTitle = self._itemIndex.addItemHeading(tHandle,
|
326
|
+
cTitle = self._itemIndex.addItemHeading(tHandle, n, hDepth, hText)
|
315
327
|
if cTitle != TT_NONE:
|
316
328
|
if nTitle > 0:
|
317
329
|
# We have a new title, so we need to count the words of the previous one
|
318
|
-
lastText = "\n".join(
|
330
|
+
lastText = "\n".join(lines[nTitle-1:n-1])
|
319
331
|
self._indexWordCounts(tHandle, lastText, pTitle)
|
320
|
-
nTitle =
|
332
|
+
nTitle = n
|
321
333
|
pTitle = cTitle
|
322
334
|
|
323
|
-
elif
|
335
|
+
elif line.startswith("@"):
|
324
336
|
if cTitle != TT_NONE:
|
325
|
-
self._indexKeyword(tHandle,
|
337
|
+
self._indexKeyword(tHandle, line, cTitle, nwItem.itemClass, tags)
|
326
338
|
|
327
|
-
elif
|
339
|
+
elif line.startswith("%"):
|
328
340
|
if cTitle != TT_NONE:
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
cLen = len(toCheck)
|
333
|
-
cOff = tLen - cLen
|
334
|
-
if synTag == "synopsis:":
|
335
|
-
sText = aLine[cOff+9:].strip()
|
336
|
-
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
|
341
|
+
cStyle, cText, _ = processComment(line)
|
342
|
+
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
|
343
|
+
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
|
337
344
|
|
338
345
|
# Count words for remaining text after last heading
|
339
346
|
if pTitle != TT_NONE:
|
340
|
-
lastText = "\n".join(
|
347
|
+
lastText = "\n".join(lines[nTitle-1:])
|
341
348
|
self._indexWordCounts(tHandle, lastText, pTitle)
|
342
349
|
|
343
350
|
# Also count words on a page with no titles
|
@@ -346,17 +353,29 @@ class NWIndex:
|
|
346
353
|
|
347
354
|
# Prune no longer used tags
|
348
355
|
for tTag, isActive in tags.items():
|
349
|
-
|
350
|
-
|
356
|
+
updated = []
|
357
|
+
deleted = []
|
358
|
+
if isActive:
|
359
|
+
logger.debug("Added/updated tag '%s'", tTag)
|
360
|
+
updated.append(tTag)
|
361
|
+
else:
|
362
|
+
logger.debug("Removed tag '%s'", tTag)
|
351
363
|
del self._tagsIndex[tTag]
|
364
|
+
deleted.append(tTag)
|
365
|
+
if updated or deleted:
|
366
|
+
SHARED.indexSignalProxy({
|
367
|
+
"event": "updateTags",
|
368
|
+
"updated": updated,
|
369
|
+
"deleted": deleted,
|
370
|
+
})
|
352
371
|
|
353
372
|
return
|
354
373
|
|
355
374
|
def _scanInactive(self, nwItem: NWItem, text: str) -> None:
|
356
375
|
"""Scan an inactive document for meta data."""
|
357
|
-
for
|
358
|
-
if
|
359
|
-
hDepth, _ = self._splitHeading(
|
376
|
+
for line in text.splitlines():
|
377
|
+
if line.startswith("#"):
|
378
|
+
hDepth, _ = self._splitHeading(line)
|
360
379
|
if hDepth != "H0":
|
361
380
|
nwItem.setMainHeading(hDepth)
|
362
381
|
break
|
@@ -378,35 +397,35 @@ class NWIndex:
|
|
378
397
|
return "H2", line[4:].strip()
|
379
398
|
return "H0", ""
|
380
399
|
|
381
|
-
def _indexWordCounts(self, tHandle: str, text: str, sTitle: str):
|
400
|
+
def _indexWordCounts(self, tHandle: str, text: str, sTitle: str) -> None:
|
382
401
|
"""Count text stats and save the counts to the index."""
|
383
402
|
cC, wC, pC = countWords(text)
|
384
403
|
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
|
385
404
|
return
|
386
405
|
|
387
406
|
def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
|
388
|
-
itemClass: nwItemClass, tags: dict) -> None:
|
407
|
+
itemClass: nwItemClass, tags: dict[str, bool]) -> None:
|
389
408
|
"""Validate and save the information about a reference to a tag
|
390
409
|
in another file, or the setting of a tag in the file. A record
|
391
410
|
of active tags is updated so that no longer used tags can be
|
392
411
|
pruned later.
|
393
412
|
"""
|
394
|
-
isValid,
|
395
|
-
if not isValid or len(
|
396
|
-
logger.warning("Skipping keyword with %d value(s) in '%s'", len(
|
413
|
+
isValid, tBits, _ = self.scanThis(line)
|
414
|
+
if not isValid or len(tBits) < 2:
|
415
|
+
logger.warning("Skipping keyword with %d value(s) in '%s'", len(tBits), tHandle)
|
397
416
|
return
|
398
417
|
|
399
|
-
if
|
400
|
-
logger.warning("Skipping invalid keyword '%s' in '%s'",
|
418
|
+
if tBits[0] not in nwKeyWords.VALID_KEYS:
|
419
|
+
logger.warning("Skipping invalid keyword '%s' in '%s'", tBits[0], tHandle)
|
401
420
|
return
|
402
421
|
|
403
|
-
if
|
404
|
-
tagName =
|
422
|
+
if tBits[0] == nwKeyWords.TAG_KEY:
|
423
|
+
tagName = tBits[1]
|
405
424
|
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
|
406
425
|
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName)
|
407
|
-
tags[tagName] = True
|
426
|
+
tags[tagName.lower()] = True
|
408
427
|
else:
|
409
|
-
self._itemIndex.
|
428
|
+
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
|
410
429
|
|
411
430
|
return
|
412
431
|
|
@@ -475,10 +494,10 @@ class NWIndex:
|
|
475
494
|
return isGood
|
476
495
|
|
477
496
|
# If we're still here, we check that the references exist
|
478
|
-
|
497
|
+
refKey = nwKeyWords.KEY_CLASS[tBits[0]].name
|
479
498
|
for n in range(1, nBits):
|
480
499
|
if tBits[n] in self._tagsIndex:
|
481
|
-
isGood[n] = self._tagsIndex.tagClass(tBits[n]) ==
|
500
|
+
isGood[n] = self._tagsIndex.tagClass(tBits[n]) == refKey
|
482
501
|
|
483
502
|
return isGood
|
484
503
|
|
@@ -504,8 +523,8 @@ class NWIndex:
|
|
504
523
|
they appear in the tree view and in the respective document
|
505
524
|
files, but skipping all note files.
|
506
525
|
"""
|
507
|
-
|
508
|
-
for tHandle, sTitle, hItem in
|
526
|
+
structure = self._itemIndex.iterNovelStructure(rHandle=rootHandle, skipExcl=skipExcl)
|
527
|
+
for tHandle, sTitle, hItem in structure:
|
509
528
|
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
|
510
529
|
return
|
511
530
|
|
@@ -555,14 +574,14 @@ class NWIndex:
|
|
555
574
|
"words": hItem.wordCount,
|
556
575
|
}
|
557
576
|
|
558
|
-
|
577
|
+
result = [(
|
559
578
|
tKey,
|
560
579
|
tData[tKey]["level"],
|
561
580
|
tData[tKey]["title"],
|
562
581
|
tData[tKey]["words"]
|
563
582
|
) for tKey in tOrder]
|
564
583
|
|
565
|
-
return
|
584
|
+
return result
|
566
585
|
|
567
586
|
def getCounts(self, tHandle: str, sTitle: str | None = None) -> tuple[int, int, int]:
|
568
587
|
"""Return the counts for a file, or a section of a file,
|
@@ -586,41 +605,66 @@ class NWIndex:
|
|
586
605
|
"""Extract all references made in a file, and optionally title
|
587
606
|
section.
|
588
607
|
"""
|
589
|
-
|
608
|
+
tRefs = {x: [] for x in nwKeyWords.KEY_CLASS}
|
590
609
|
for rTitle, hItem in self._itemIndex.iterItemHeaders(tHandle):
|
591
610
|
if sTitle is None or sTitle == rTitle:
|
592
611
|
for aTag, refTypes in hItem.references.items():
|
593
612
|
for refType in refTypes:
|
594
|
-
if refType in
|
595
|
-
|
613
|
+
if refType in tRefs:
|
614
|
+
tRefs[refType].append(self._tagsIndex.tagName(aTag))
|
596
615
|
|
597
|
-
return
|
616
|
+
return tRefs
|
598
617
|
|
599
|
-
def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
|
600
|
-
"""Build a
|
601
|
-
by tHandle.
|
602
|
-
"""
|
618
|
+
def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
|
619
|
+
"""Build a dict of files referring back to our file."""
|
603
620
|
if tHandle is None or tHandle not in self._itemIndex:
|
604
621
|
return {}
|
605
622
|
|
606
|
-
|
607
|
-
|
608
|
-
if not
|
609
|
-
return
|
623
|
+
tRefs = {}
|
624
|
+
tTags = self._itemIndex.allItemTags(tHandle)
|
625
|
+
if not tTags:
|
626
|
+
return tRefs
|
610
627
|
|
611
628
|
for aHandle, sTitle, hItem in self._itemIndex.iterAllHeaders():
|
612
629
|
for aTag in hItem.references:
|
613
|
-
if aTag in
|
614
|
-
|
630
|
+
if aTag in tTags and aHandle not in tRefs:
|
631
|
+
tRefs[aHandle] = (sTitle, hItem)
|
615
632
|
|
616
|
-
return
|
633
|
+
return tRefs
|
617
634
|
|
618
|
-
def getTagSource(self, tagKey: str) -> tuple[str, str]:
|
635
|
+
def getTagSource(self, tagKey: str) -> tuple[str | None, str]:
|
619
636
|
"""Return the source location of a given tag."""
|
620
637
|
tHandle = self._tagsIndex.tagHandle(tagKey)
|
621
638
|
sTitle = self._tagsIndex.tagHeading(tagKey)
|
622
639
|
return tHandle, sTitle
|
623
640
|
|
641
|
+
def getDocumentTags(self, tHandle: str | None) -> list[str]:
|
642
|
+
"""Return all tags used by a specific document."""
|
643
|
+
return self._itemIndex.allItemTags(tHandle) if tHandle else []
|
644
|
+
|
645
|
+
def getClassTags(self, itemClass: nwItemClass) -> list[str]:
|
646
|
+
"""Return all tags based on itemClass."""
|
647
|
+
return self._tagsIndex.filterTagNames(itemClass.name)
|
648
|
+
|
649
|
+
def getTagsData(self) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
|
650
|
+
"""Return all known tags."""
|
651
|
+
for tag, data in self._tagsIndex.items():
|
652
|
+
iItem = self._itemIndex[data.get("handle")]
|
653
|
+
hItem = None if iItem is None else iItem[data.get("heading")]
|
654
|
+
yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem
|
655
|
+
return
|
656
|
+
|
657
|
+
def getSingleTag(self, tagKey: str) -> tuple[str, str, IndexItem | None, IndexHeading | None]:
|
658
|
+
"""Return tag data for a specific tag."""
|
659
|
+
tName = self._tagsIndex.tagName(tagKey)
|
660
|
+
tClass = self._tagsIndex.tagClass(tagKey)
|
661
|
+
tHandle = self._tagsIndex.tagHandle(tagKey)
|
662
|
+
tHeading = self._tagsIndex.tagHeading(tagKey)
|
663
|
+
if tName and tClass and tHandle and tHeading:
|
664
|
+
iItem = self._itemIndex[tHandle]
|
665
|
+
return tName, tClass, iItem, None if iItem is None else iItem[tHeading]
|
666
|
+
return "", "", None, None
|
667
|
+
|
624
668
|
# END Class NWIndex
|
625
669
|
|
626
670
|
|
@@ -638,47 +682,61 @@ class TagsIndex:
|
|
638
682
|
|
639
683
|
__slots__ = ("_tags")
|
640
684
|
|
641
|
-
def __init__(self):
|
642
|
-
self._tags: dict[str, dict] = {}
|
685
|
+
def __init__(self) -> None:
|
686
|
+
self._tags: dict[str, dict[str, str]] = {}
|
643
687
|
return
|
644
688
|
|
645
|
-
def __contains__(self, tagKey):
|
646
|
-
return tagKey in self._tags
|
689
|
+
def __contains__(self, tagKey: str) -> bool:
|
690
|
+
return tagKey.lower() in self._tags
|
647
691
|
|
648
|
-
def __delitem__(self, tagKey):
|
649
|
-
self._tags.pop(tagKey, None)
|
692
|
+
def __delitem__(self, tagKey: str) -> None:
|
693
|
+
self._tags.pop(tagKey.lower(), None)
|
650
694
|
return
|
651
695
|
|
652
|
-
def __getitem__(self, tagKey):
|
653
|
-
return self._tags.get(tagKey, None)
|
696
|
+
def __getitem__(self, tagKey: str) -> dict | None:
|
697
|
+
return self._tags.get(tagKey.lower(), None)
|
654
698
|
|
655
699
|
##
|
656
700
|
# Methods
|
657
701
|
##
|
658
702
|
|
659
|
-
def clear(self):
|
703
|
+
def clear(self) -> None:
|
660
704
|
"""Clear the index."""
|
661
705
|
self._tags = {}
|
662
706
|
return
|
663
707
|
|
664
|
-
def
|
708
|
+
def items(self) -> ItemsView:
|
709
|
+
"""Return a dictionary view of all tags."""
|
710
|
+
return self._tags.items()
|
711
|
+
|
712
|
+
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None:
|
665
713
|
"""Add a key to the index and set all values."""
|
666
|
-
self._tags[tagKey] = {
|
667
|
-
"handle": tHandle, "heading": sTitle, "class": itemClass.name
|
714
|
+
self._tags[tagKey.lower()] = {
|
715
|
+
"name": tagKey, "handle": tHandle, "heading": sTitle, "class": itemClass.name
|
668
716
|
}
|
669
717
|
return
|
670
718
|
|
671
|
-
def
|
719
|
+
def tagName(self, tagKey: str) -> str:
|
720
|
+
"""Get the display name of a given tag."""
|
721
|
+
return self._tags.get(tagKey.lower(), {}).get("name", "")
|
722
|
+
|
723
|
+
def tagHandle(self, tagKey: str) -> str | None:
|
672
724
|
"""Get the handle of a given tag."""
|
673
|
-
return self._tags.get(tagKey, {}).get("handle", None)
|
725
|
+
return self._tags.get(tagKey.lower(), {}).get("handle", None)
|
674
726
|
|
675
727
|
def tagHeading(self, tagKey: str) -> str:
|
676
728
|
"""Get the heading of a given tag."""
|
677
|
-
return self._tags.get(tagKey, {}).get("heading", TT_NONE)
|
729
|
+
return self._tags.get(tagKey.lower(), {}).get("heading", TT_NONE)
|
678
730
|
|
679
731
|
def tagClass(self, tagKey: str) -> str | None:
|
680
732
|
"""Get the class of a given tag."""
|
681
|
-
return self._tags.get(tagKey, {}).get("class", None)
|
733
|
+
return self._tags.get(tagKey.lower(), {}).get("class", None)
|
734
|
+
|
735
|
+
def filterTagNames(self, className: str) -> list[str]:
|
736
|
+
"""Get a list of tag names for a given class."""
|
737
|
+
return [
|
738
|
+
x.get("name", "") for x in self._tags.values() if x.get("class", "") == className
|
739
|
+
]
|
682
740
|
|
683
741
|
##
|
684
742
|
# Pack/Unpack
|
@@ -688,7 +746,7 @@ class TagsIndex:
|
|
688
746
|
"""Pack all the data of the tags into a single dictionary."""
|
689
747
|
return self._tags
|
690
748
|
|
691
|
-
def unpackData(self, data: dict):
|
749
|
+
def unpackData(self, data: dict) -> None:
|
692
750
|
"""Iterate through the tagsIndex loaded from cache and check
|
693
751
|
that it's valid.
|
694
752
|
"""
|
@@ -699,12 +757,16 @@ class TagsIndex:
|
|
699
757
|
for tagKey, tagData in data.items():
|
700
758
|
if not isinstance(tagKey, str):
|
701
759
|
raise ValueError("tagsIndex keys must be a strings")
|
760
|
+
if "name" not in tagData:
|
761
|
+
raise KeyError("A tagIndex item is missing a name entry")
|
702
762
|
if "handle" not in tagData:
|
703
763
|
raise KeyError("A tagIndex item is missing a handle entry")
|
704
764
|
if "heading" not in tagData:
|
705
765
|
raise KeyError("A tagIndex item is missing a heading entry")
|
706
766
|
if "class" not in tagData:
|
707
767
|
raise KeyError("A tagIndex item is missing a class entry")
|
768
|
+
if tagData["name"].lower() != tagKey:
|
769
|
+
raise ValueError("tagsIndex name must match key")
|
708
770
|
if not isHandle(tagData["handle"]):
|
709
771
|
raise ValueError("tagsIndex handle must be a handle")
|
710
772
|
if not isTitleTag(tagData["heading"]):
|
@@ -735,7 +797,7 @@ class ItemIndex:
|
|
735
797
|
|
736
798
|
__slots__ = ("_project", "_items")
|
737
799
|
|
738
|
-
def __init__(self, project: NWProject):
|
800
|
+
def __init__(self, project: NWProject) -> None:
|
739
801
|
self._project = project
|
740
802
|
self._items: dict[str, IndexItem] = {}
|
741
803
|
return
|
@@ -743,7 +805,7 @@ class ItemIndex:
|
|
743
805
|
def __contains__(self, tHandle: str) -> bool:
|
744
806
|
return tHandle in self._items
|
745
807
|
|
746
|
-
def __delitem__(self, tHandle: str):
|
808
|
+
def __delitem__(self, tHandle: str) -> None:
|
747
809
|
self._items.pop(tHandle, None)
|
748
810
|
return
|
749
811
|
|
@@ -754,12 +816,12 @@ class ItemIndex:
|
|
754
816
|
# Methods
|
755
817
|
##
|
756
818
|
|
757
|
-
def clear(self):
|
819
|
+
def clear(self) -> None:
|
758
820
|
"""Clear the index."""
|
759
821
|
self._items = {}
|
760
822
|
return
|
761
823
|
|
762
|
-
def add(self, tHandle: str, nwItem: NWItem):
|
824
|
+
def add(self, tHandle: str, nwItem: NWItem) -> None:
|
763
825
|
"""Add a new item to the index. This will overwrite the item if
|
764
826
|
it already exists.
|
765
827
|
"""
|
@@ -827,7 +889,7 @@ class ItemIndex:
|
|
827
889
|
return sTitle
|
828
890
|
return TT_NONE
|
829
891
|
|
830
|
-
def setHeadingCounts(self, tHandle: str, sTitle: str, cC: int, wC: int, pC: int):
|
892
|
+
def setHeadingCounts(self, tHandle: str, sTitle: str, cC: int, wC: int, pC: int) -> None:
|
831
893
|
"""Set the character, word and paragraph counts of a heading
|
832
894
|
on a given item.
|
833
895
|
"""
|
@@ -835,22 +897,22 @@ class ItemIndex:
|
|
835
897
|
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
|
836
898
|
return
|
837
899
|
|
838
|
-
def setHeadingSynopsis(self, tHandle: str, sTitle: str, text: str):
|
900
|
+
def setHeadingSynopsis(self, tHandle: str, sTitle: str, text: str) -> None:
|
839
901
|
"""Set the synopsis text for a heading on a given item."""
|
840
902
|
if tHandle in self._items:
|
841
903
|
self._items[tHandle].setHeadingSynopsis(sTitle, text)
|
842
904
|
return
|
843
905
|
|
844
|
-
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str):
|
906
|
+
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
|
845
907
|
"""Set the main tag for a heading on a given item."""
|
846
908
|
if tHandle in self._items:
|
847
909
|
self._items[tHandle].setHeadingTag(sTitle, tagKey)
|
848
910
|
return
|
849
911
|
|
850
|
-
def
|
912
|
+
def addHeadingRef(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str) -> None:
|
851
913
|
"""Set the reference tags for a heading on a given item."""
|
852
914
|
if tHandle in self._items:
|
853
|
-
self._items[tHandle].
|
915
|
+
self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType)
|
854
916
|
return
|
855
917
|
|
856
918
|
##
|
@@ -861,7 +923,7 @@ class ItemIndex:
|
|
861
923
|
"""Pack all the data of the index into a single dictionary."""
|
862
924
|
return {handle: item.packData() for handle, item in self._items.items()}
|
863
925
|
|
864
|
-
def unpackData(self, data: dict):
|
926
|
+
def unpackData(self, data: dict) -> None:
|
865
927
|
"""Iterate through the itemIndex loaded from cache and check
|
866
928
|
that it's valid. This will raise errors if there is a problem.
|
867
929
|
"""
|
@@ -894,17 +956,13 @@ class IndexItem:
|
|
894
956
|
must be reset each time the item is re-indexed.
|
895
957
|
"""
|
896
958
|
|
897
|
-
__slots__ = ("_handle", "_item", "_headings", "
|
959
|
+
__slots__ = ("_handle", "_item", "_headings", "_count")
|
898
960
|
|
899
|
-
def __init__(self, tHandle: str, nwItem: NWItem):
|
961
|
+
def __init__(self, tHandle: str, nwItem: NWItem) -> None:
|
900
962
|
self._handle = tHandle
|
901
963
|
self._item = nwItem
|
902
|
-
self._headings: dict[str, IndexHeading] = {}
|
964
|
+
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(TT_NONE)}
|
903
965
|
self._count = 0
|
904
|
-
|
905
|
-
# Add a placeholder heading
|
906
|
-
self._headings[TT_NONE] = IndexHeading(TT_NONE)
|
907
|
-
|
908
966
|
return
|
909
967
|
|
910
968
|
def __repr__(self) -> str:
|
@@ -923,15 +981,21 @@ class IndexItem:
|
|
923
981
|
# Properties
|
924
982
|
##
|
925
983
|
|
984
|
+
@property
|
985
|
+
def handle(self) -> str:
|
986
|
+
"""Return the item handle of the index item."""
|
987
|
+
return self._handle
|
988
|
+
|
926
989
|
@property
|
927
990
|
def item(self) -> NWItem:
|
991
|
+
"""Return the project item of the index item."""
|
928
992
|
return self._item
|
929
993
|
|
930
994
|
##
|
931
995
|
# Setters
|
932
996
|
##
|
933
997
|
|
934
|
-
def addHeading(self, tHeading: IndexHeading):
|
998
|
+
def addHeading(self, tHeading: IndexHeading) -> None:
|
935
999
|
"""Add a heading to the item. Also remove the placeholder entry
|
936
1000
|
if it exists.
|
937
1001
|
"""
|
@@ -940,25 +1004,25 @@ class IndexItem:
|
|
940
1004
|
self._headings[tHeading.key] = tHeading
|
941
1005
|
return
|
942
1006
|
|
943
|
-
def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int):
|
1007
|
+
def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int) -> None:
|
944
1008
|
"""Set the character, word and paragraph count of a heading."""
|
945
1009
|
if sTitle in self._headings:
|
946
1010
|
self._headings[sTitle].setCounts(cCount, wCount, pCount)
|
947
1011
|
return
|
948
1012
|
|
949
|
-
def setHeadingSynopsis(self, sTitle: str, text: str):
|
1013
|
+
def setHeadingSynopsis(self, sTitle: str, text: str) -> None:
|
950
1014
|
"""Set the synopsis text of a heading."""
|
951
1015
|
if sTitle in self._headings:
|
952
1016
|
self._headings[sTitle].setSynopsis(text)
|
953
1017
|
return
|
954
1018
|
|
955
|
-
def setHeadingTag(self, sTitle: str, tagKey: str):
|
1019
|
+
def setHeadingTag(self, sTitle: str, tagKey: str) -> None:
|
956
1020
|
"""Set the tag of a heading."""
|
957
1021
|
if sTitle in self._headings:
|
958
1022
|
self._headings[sTitle].setTag(tagKey)
|
959
1023
|
return
|
960
1024
|
|
961
|
-
def
|
1025
|
+
def addHeadingRef(self, sTitle: str, tagKeys: list[str], refType: str) -> None:
|
962
1026
|
"""Add a reference key and all its types to a heading."""
|
963
1027
|
if sTitle in self._headings:
|
964
1028
|
for tagKey in tagKeys:
|
@@ -970,9 +1034,11 @@ class IndexItem:
|
|
970
1034
|
##
|
971
1035
|
|
972
1036
|
def items(self) -> ItemsView[str, IndexHeading]:
|
1037
|
+
"""Return IndexHeading items."""
|
973
1038
|
return self._headings.items()
|
974
1039
|
|
975
1040
|
def headings(self) -> list[str]:
|
1041
|
+
"""Return heading keys in sorted order."""
|
976
1042
|
return sorted(self._headings.keys())
|
977
1043
|
|
978
1044
|
def allTags(self) -> list[str]:
|
@@ -1005,7 +1071,7 @@ class IndexItem:
|
|
1005
1071
|
|
1006
1072
|
return data
|
1007
1073
|
|
1008
|
-
def unpackData(self, data: dict):
|
1074
|
+
def unpackData(self, data: dict) -> None:
|
1009
1075
|
"""Unpack an item entry from the data."""
|
1010
1076
|
references = data.get("references", {})
|
1011
1077
|
for sTitle, hData in data.get("headings", {}).items():
|
@@ -1033,7 +1099,7 @@ class IndexHeading:
|
|
1033
1099
|
"_paraCount", "_synopsis", "_tag", "_refs",
|
1034
1100
|
)
|
1035
1101
|
|
1036
|
-
def __init__(self, key: str, line: int = 0, level: str = "H0", title: str = ""):
|
1102
|
+
def __init__(self, key: str, line: int = 0, level: str = "H0", title: str = "") -> None:
|
1037
1103
|
self._key = key
|
1038
1104
|
self._line = line
|
1039
1105
|
self._level = level
|
@@ -1100,18 +1166,18 @@ class IndexHeading:
|
|
1100
1166
|
# Setters
|
1101
1167
|
##
|
1102
1168
|
|
1103
|
-
def setLevel(self, level: str):
|
1169
|
+
def setLevel(self, level: str) -> None:
|
1104
1170
|
"""Set the level of the header if it's a valid value."""
|
1105
1171
|
if level in nwHeaders.H_VALID:
|
1106
1172
|
self._level = level
|
1107
1173
|
return
|
1108
1174
|
|
1109
|
-
def setLine(self, line: int):
|
1175
|
+
def setLine(self, line: int) -> None:
|
1110
1176
|
"""Set the line number of a heading."""
|
1111
1177
|
self._line = max(0, checkInt(line, 0))
|
1112
1178
|
return
|
1113
1179
|
|
1114
|
-
def setCounts(self, charCount: int, wordCount: int, paraCount: int):
|
1180
|
+
def setCounts(self, charCount: int, wordCount: int, paraCount: int) -> None:
|
1115
1181
|
"""Set the character, word and paragraph count. Make sure the
|
1116
1182
|
value is an integer and is not smaller than 0.
|
1117
1183
|
"""
|
@@ -1120,21 +1186,22 @@ class IndexHeading:
|
|
1120
1186
|
self._paraCount = max(0, checkInt(paraCount, 0))
|
1121
1187
|
return
|
1122
1188
|
|
1123
|
-
def setSynopsis(self, text: str):
|
1189
|
+
def setSynopsis(self, text: str) -> None:
|
1124
1190
|
"""Set the synopsis text and make sure it is a string."""
|
1125
1191
|
self._synopsis = str(text)
|
1126
1192
|
return
|
1127
1193
|
|
1128
|
-
def setTag(self, tagKey: str):
|
1194
|
+
def setTag(self, tagKey: str) -> None:
|
1129
1195
|
"""Set the tag for references, and make sure it is a string."""
|
1130
|
-
self._tag = str(tagKey)
|
1196
|
+
self._tag = str(tagKey).lower()
|
1131
1197
|
return
|
1132
1198
|
|
1133
|
-
def addReference(self, tagKey: str, refType: str):
|
1199
|
+
def addReference(self, tagKey: str, refType: str) -> None:
|
1134
1200
|
"""Add a record of a reference tag, and what keyword types it is
|
1135
1201
|
associated with.
|
1136
1202
|
"""
|
1137
1203
|
if refType in nwKeyWords.VALID_KEYS:
|
1204
|
+
tagKey = tagKey.lower()
|
1138
1205
|
if tagKey not in self._refs:
|
1139
1206
|
self._refs[tagKey] = set()
|
1140
1207
|
self._refs[tagKey].add(refType)
|
@@ -1165,7 +1232,7 @@ class IndexHeading:
|
|
1165
1232
|
"""
|
1166
1233
|
return {key: ",".join(sorted(list(value))) for key, value in self._refs.items()}
|
1167
1234
|
|
1168
|
-
def unpackData(self, data: dict):
|
1235
|
+
def unpackData(self, data: dict) -> None:
|
1169
1236
|
"""Unpack a heading entry from a dictionary."""
|
1170
1237
|
self.setLevel(data.get("level", "H0"))
|
1171
1238
|
self._title = str(data.get("title", ""))
|
@@ -1179,7 +1246,7 @@ class IndexHeading:
|
|
1179
1246
|
self._synopsis = str(data.get("synopsis", ""))
|
1180
1247
|
return
|
1181
1248
|
|
1182
|
-
def unpackReferences(self, data: dict):
|
1249
|
+
def unpackReferences(self, data: dict) -> None:
|
1183
1250
|
"""Unpack a set of references from a dictionary."""
|
1184
1251
|
for tagKey, refTypes in data.items():
|
1185
1252
|
if not isinstance(tagKey, str):
|
@@ -1197,9 +1264,26 @@ class IndexHeading:
|
|
1197
1264
|
|
1198
1265
|
|
1199
1266
|
# =============================================================================================== #
|
1200
|
-
#
|
1267
|
+
# Text Processing Functions
|
1201
1268
|
# =============================================================================================== #
|
1202
1269
|
|
1270
|
+
CLASSIFIERS = {
|
1271
|
+
"short": nwComment.SHORT,
|
1272
|
+
"synopsis": nwComment.SYNOPSIS,
|
1273
|
+
}
|
1274
|
+
|
1275
|
+
|
1276
|
+
def processComment(text: str) -> tuple[nwComment, str, int]:
|
1277
|
+
"""Extract comment style and text. Should only be called on text
|
1278
|
+
starting with a %.
|
1279
|
+
"""
|
1280
|
+
check = text[1:].lstrip()
|
1281
|
+
classifier, _, content = check.partition(":")
|
1282
|
+
if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
|
1283
|
+
return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
|
1284
|
+
return nwComment.PLAIN, check, 0
|
1285
|
+
|
1286
|
+
|
1203
1287
|
def countWords(text: str) -> tuple[int, int, int]:
|
1204
1288
|
"""Count words in a piece of text, skipping special syntax and
|
1205
1289
|
comments.
|
@@ -1221,55 +1305,60 @@ def countWords(text: str) -> tuple[int, int, int]:
|
|
1221
1305
|
if nwUnicode.U_EMDASH in text:
|
1222
1306
|
text = text.replace(nwUnicode.U_EMDASH, " ")
|
1223
1307
|
|
1224
|
-
|
1308
|
+
# Strip shortcodes
|
1309
|
+
if "[" in text:
|
1310
|
+
text = nwRegEx.RX_SC.sub("", text)
|
1311
|
+
|
1312
|
+
for line in text.splitlines():
|
1225
1313
|
|
1226
1314
|
countPara = True
|
1227
1315
|
|
1228
|
-
if not
|
1316
|
+
if not line:
|
1229
1317
|
prevEmpty = True
|
1230
1318
|
continue
|
1231
1319
|
|
1232
|
-
if
|
1320
|
+
if line[0] == "@" or line[0] == "%":
|
1233
1321
|
continue
|
1234
1322
|
|
1235
|
-
if
|
1236
|
-
|
1323
|
+
if line[0] == "[":
|
1324
|
+
check = line.lower()
|
1325
|
+
if check.startswith(("[newpage]", "[new page]", "[vspace]")):
|
1237
1326
|
continue
|
1238
|
-
elif
|
1327
|
+
elif check.startswith("[vspace:") and line.endswith("]"):
|
1239
1328
|
continue
|
1240
1329
|
|
1241
|
-
elif
|
1242
|
-
if
|
1243
|
-
|
1330
|
+
elif line[0] == "#":
|
1331
|
+
if line[:5] == "#### ":
|
1332
|
+
line = line[5:]
|
1244
1333
|
countPara = False
|
1245
|
-
elif
|
1246
|
-
|
1334
|
+
elif line[:4] == "### ":
|
1335
|
+
line = line[4:]
|
1247
1336
|
countPara = False
|
1248
|
-
elif
|
1249
|
-
|
1337
|
+
elif line[:3] == "## ":
|
1338
|
+
line = line[3:]
|
1250
1339
|
countPara = False
|
1251
|
-
elif
|
1252
|
-
|
1340
|
+
elif line[:2] == "# ":
|
1341
|
+
line = line[2:]
|
1253
1342
|
countPara = False
|
1254
|
-
elif
|
1255
|
-
|
1343
|
+
elif line[:3] == "#! ":
|
1344
|
+
line = line[3:]
|
1256
1345
|
countPara = False
|
1257
|
-
elif
|
1258
|
-
|
1346
|
+
elif line[:4] == "##! ":
|
1347
|
+
line = line[4:]
|
1259
1348
|
countPara = False
|
1260
1349
|
|
1261
|
-
elif
|
1262
|
-
if
|
1263
|
-
|
1264
|
-
elif
|
1265
|
-
|
1266
|
-
if
|
1267
|
-
|
1268
|
-
elif
|
1269
|
-
|
1270
|
-
|
1271
|
-
wordCount += len(
|
1272
|
-
charCount += len(
|
1350
|
+
elif line[0] == ">" or line[-1] == "<":
|
1351
|
+
if line[:2] == ">>":
|
1352
|
+
line = line[2:].lstrip(" ")
|
1353
|
+
elif line[:1] == ">":
|
1354
|
+
line = line[1:].lstrip(" ")
|
1355
|
+
if line[-2:] == "<<":
|
1356
|
+
line = line[:-2].rstrip(" ")
|
1357
|
+
elif line[-1:] == "<":
|
1358
|
+
line = line[:-1].rstrip(" ")
|
1359
|
+
|
1360
|
+
wordCount += len(line.split())
|
1361
|
+
charCount += len(line)
|
1273
1362
|
if countPara and prevEmpty:
|
1274
1363
|
paraCount += 1
|
1275
1364
|
|