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.
Files changed (109) hide show
  1. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
  3. novelwriter/__init__.py +6 -24
  4. novelwriter/assets/i18n/project_de_DE.json +10 -0
  5. novelwriter/assets/i18n/project_en_GB.json +11 -0
  6. novelwriter/assets/i18n/project_en_US.json +10 -0
  7. novelwriter/assets/i18n/project_ja_JP.json +11 -1
  8. novelwriter/assets/i18n/project_nb_NO.json +10 -0
  9. novelwriter/assets/i18n/project_nn_NO.json +10 -0
  10. novelwriter/assets/icons/novelwriter.ico +0 -0
  11. novelwriter/assets/icons/novelwriter.svg +8 -183
  12. novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
  13. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  14. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
  21. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
  22. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
  25. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
  26. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/icons.conf +17 -2
  29. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  30. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  33. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
  35. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
  37. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
  40. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
  42. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
  44. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  45. novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
  46. novelwriter/assets/manual.pdf +0 -0
  47. novelwriter/assets/sample.zip +0 -0
  48. novelwriter/assets/syntax/default_dark.conf +1 -0
  49. novelwriter/assets/syntax/default_light.conf +1 -0
  50. novelwriter/assets/syntax/grey_dark.conf +1 -0
  51. novelwriter/assets/syntax/grey_light.conf +1 -0
  52. novelwriter/assets/syntax/light_owl.conf +1 -0
  53. novelwriter/assets/syntax/night_owl.conf +1 -0
  54. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  55. novelwriter/assets/syntax/solarized_light.conf +1 -0
  56. novelwriter/assets/syntax/tomorrow.conf +1 -0
  57. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  58. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  59. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  60. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  61. novelwriter/assets/text/credits_en.htm +7 -0
  62. novelwriter/assets/text/release_notes.htm +7 -37
  63. novelwriter/common.py +22 -1
  64. novelwriter/config.py +27 -42
  65. novelwriter/constants.py +45 -7
  66. novelwriter/core/buildsettings.py +40 -24
  67. novelwriter/core/coretools.py +8 -1
  68. novelwriter/core/docbuild.py +2 -6
  69. novelwriter/core/index.py +264 -175
  70. novelwriter/core/options.py +8 -3
  71. novelwriter/core/project.py +2 -2
  72. novelwriter/core/projectdata.py +3 -3
  73. novelwriter/core/tohtml.py +60 -59
  74. novelwriter/core/tokenizer.py +110 -70
  75. novelwriter/core/tomd.py +51 -38
  76. novelwriter/core/toodt.py +184 -147
  77. novelwriter/dialogs/preferences.py +75 -106
  78. novelwriter/dialogs/projsettings.py +101 -110
  79. novelwriter/dialogs/updates.py +25 -14
  80. novelwriter/enum.py +28 -3
  81. novelwriter/extensions/novelselector.py +1 -1
  82. novelwriter/gui/doceditor.py +1345 -1235
  83. novelwriter/gui/dochighlight.py +98 -62
  84. novelwriter/gui/docviewer.py +151 -340
  85. novelwriter/gui/docviewerpanel.py +457 -0
  86. novelwriter/gui/editordocument.py +126 -0
  87. novelwriter/gui/mainmenu.py +350 -300
  88. novelwriter/gui/noveltree.py +101 -125
  89. novelwriter/gui/outline.py +154 -171
  90. novelwriter/gui/projtree.py +480 -380
  91. novelwriter/gui/sidebar.py +106 -75
  92. novelwriter/gui/statusbar.py +1 -1
  93. novelwriter/gui/theme.py +114 -75
  94. novelwriter/guimain.py +353 -254
  95. novelwriter/shared.py +36 -3
  96. novelwriter/tools/dictionaries.py +268 -0
  97. novelwriter/tools/manusbuild.py +17 -6
  98. novelwriter/tools/manuscript.py +11 -3
  99. novelwriter/tools/manussettings.py +0 -14
  100. novelwriter/tools/projwizard.py +16 -2
  101. novelwriter/tools/writingstats.py +1 -1
  102. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  103. novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
  104. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  105. novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
  106. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  109. {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.enum import nwItemClass, nwItemType, nwItemLayout
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
- for tTag in self._itemIndex.allItemTags(tHandle):
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 is None or not self._project.tree.checkType(tHandle, nwItemType.FILE):
142
- return False
143
-
144
- logger.debug("Re-indexing item '%s'", tHandle)
145
- theDoc = self._project.storage.getDocument(tHandle)
146
- self.scanText(tHandle, theDoc.readDocument() or "")
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
- return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
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
- theData = json.load(inFile)
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(theData["novelWriter.tagsIndex"])
187
- self._itemIndex.unpackData(theData["novelWriter.itemIndex"])
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, theText: str) -> bool:
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
- theItem = self._project.tree[tHandle]
249
- if theItem is None:
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 theItem.isFileType():
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, theItem)
265
+ self._itemIndex.add(tHandle, tItem)
259
266
 
260
267
  # Run word counter for the whole text
261
- cC, wC, pC = countWords(theText)
262
- theItem.setCharCount(cC)
263
- theItem.setWordCount(wC)
264
- theItem.setParaCount(pC)
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 theItem.itemLayout == nwItemLayout.NO_LAYOUT:
275
+ if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
269
276
  logger.info("Not indexing no-layout item '%s'", tHandle)
270
277
  return False
271
- if theItem.itemParent is None:
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 theItem.isInactiveClass():
277
- self._scanInactive(theItem, theText)
283
+ if tItem.isInactiveClass():
284
+ self._scanInactive(tItem, text)
278
285
  else:
279
- self._scanActive(tHandle, theItem, theText, itemTags)
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[theItem.itemRoot] = nowTime
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
- theLines = text.splitlines()
300
- for nLine, aLine in enumerate(theLines, start=1):
311
+ lines = text.splitlines()
312
+ for n, line in enumerate(lines, start=1):
301
313
 
302
- if aLine.strip() == "":
314
+ if line.strip() == "":
303
315
  continue
304
316
 
305
- if aLine.startswith("#"):
306
- hDepth, hText = self._splitHeading(aLine)
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, nLine, hDepth, hText)
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(theLines[nTitle-1:nLine-1])
330
+ lastText = "\n".join(lines[nTitle-1:n-1])
319
331
  self._indexWordCounts(tHandle, lastText, pTitle)
320
- nTitle = nLine
332
+ nTitle = n
321
333
  pTitle = cTitle
322
334
 
323
- elif aLine.startswith("@"):
335
+ elif line.startswith("@"):
324
336
  if cTitle != TT_NONE:
325
- self._indexKeyword(tHandle, aLine, cTitle, nwItem.itemClass, tags)
337
+ self._indexKeyword(tHandle, line, cTitle, nwItem.itemClass, tags)
326
338
 
327
- elif aLine.startswith("%"):
339
+ elif line.startswith("%"):
328
340
  if cTitle != TT_NONE:
329
- toCheck = aLine[1:].lstrip()
330
- synTag = toCheck[:9].lower()
331
- tLen = len(aLine)
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(theLines[nTitle-1:])
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
- if not isActive:
350
- logger.debug("Deleting removed tag '%s'", tTag)
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 aLine in text.splitlines():
358
- if aLine.startswith("#"):
359
- hDepth, _ = self._splitHeading(aLine)
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, theBits, _ = self.scanThis(line)
395
- if not isValid or len(theBits) < 2:
396
- logger.warning("Skipping keyword with %d value(s) in '%s'", len(theBits), tHandle)
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 theBits[0] not in nwKeyWords.VALID_KEYS:
400
- logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
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 theBits[0] == nwKeyWords.TAG_KEY:
404
- tagName = theBits[1]
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.addHeadingReferences(tHandle, sTitle, theBits[1:], theBits[0])
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
- theKey = nwKeyWords.KEY_CLASS[tBits[0]].name
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]) == theKey
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
- novStruct = self._itemIndex.iterNovelStructure(rHandle=rootHandle, skipExcl=skipExcl)
508
- for tHandle, sTitle, hItem in novStruct:
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
- theToC = [(
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 theToC
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
- theRefs = {x: [] for x in nwKeyWords.KEY_CLASS}
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 theRefs:
595
- theRefs[refType].append(aTag)
613
+ if refType in tRefs:
614
+ tRefs[refType].append(self._tagsIndex.tagName(aTag))
596
615
 
597
- return theRefs
616
+ return tRefs
598
617
 
599
- def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
600
- """Build a list of files referring back to our file, specified
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
- theRefs = {}
607
- theTags = self._itemIndex.allItemTags(tHandle)
608
- if not theTags:
609
- return theRefs
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 theTags and aHandle not in theRefs:
614
- theRefs[aHandle] = sTitle
630
+ if aTag in tTags and aHandle not in tRefs:
631
+ tRefs[aHandle] = (sTitle, hItem)
615
632
 
616
- return theRefs
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 add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass):
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 tagHandle(self, tagKey: str) -> str:
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 addHeadingReferences(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str):
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].addHeadingReferences(sTitle, tagKeys, refType)
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", "_headings", "_count")
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 addHeadingReferences(self, sTitle: str, tagKeys: list[str], refType: str):
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
- # Simple Word Counter
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
- for aLine in text.splitlines():
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 aLine:
1316
+ if not line:
1229
1317
  prevEmpty = True
1230
1318
  continue
1231
1319
 
1232
- if aLine[0] == "@" or aLine[0] == "%":
1320
+ if line[0] == "@" or line[0] == "%":
1233
1321
  continue
1234
1322
 
1235
- if aLine[0] == "[":
1236
- if aLine.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
1323
+ if line[0] == "[":
1324
+ check = line.lower()
1325
+ if check.startswith(("[newpage]", "[new page]", "[vspace]")):
1237
1326
  continue
1238
- elif aLine.startswith("[VSPACE:") and aLine.endswith("]"):
1327
+ elif check.startswith("[vspace:") and line.endswith("]"):
1239
1328
  continue
1240
1329
 
1241
- elif aLine[0] == "#":
1242
- if aLine[:5] == "#### ":
1243
- aLine = aLine[5:]
1330
+ elif line[0] == "#":
1331
+ if line[:5] == "#### ":
1332
+ line = line[5:]
1244
1333
  countPara = False
1245
- elif aLine[:4] == "### ":
1246
- aLine = aLine[4:]
1334
+ elif line[:4] == "### ":
1335
+ line = line[4:]
1247
1336
  countPara = False
1248
- elif aLine[:3] == "## ":
1249
- aLine = aLine[3:]
1337
+ elif line[:3] == "## ":
1338
+ line = line[3:]
1250
1339
  countPara = False
1251
- elif aLine[:2] == "# ":
1252
- aLine = aLine[2:]
1340
+ elif line[:2] == "# ":
1341
+ line = line[2:]
1253
1342
  countPara = False
1254
- elif aLine[:3] == "#! ":
1255
- aLine = aLine[3:]
1343
+ elif line[:3] == "#! ":
1344
+ line = line[3:]
1256
1345
  countPara = False
1257
- elif aLine[:4] == "##! ":
1258
- aLine = aLine[4:]
1346
+ elif line[:4] == "##! ":
1347
+ line = line[4:]
1259
1348
  countPara = False
1260
1349
 
1261
- elif aLine[0] == ">" or aLine[-1] == "<":
1262
- if aLine[:2] == ">>":
1263
- aLine = aLine[2:].lstrip(" ")
1264
- elif aLine[:1] == ">":
1265
- aLine = aLine[1:].lstrip(" ")
1266
- if aLine[-2:] == "<<":
1267
- aLine = aLine[:-2].rstrip(" ")
1268
- elif aLine[-1:] == "<":
1269
- aLine = aLine[:-1].rstrip(" ")
1270
-
1271
- wordCount += len(aLine.split())
1272
- charCount += len(aLine)
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