novelWriter 2.3.1__py3-none-any.whl → 2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/RECORD +114 -107
- novelwriter/__init__.py +17 -10
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- 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 +12 -4
- novelwriter/config.py +47 -16
- novelwriter/constants.py +5 -6
- novelwriter/core/buildsettings.py +64 -44
- 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/projectxml.py +1 -1
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/spellcheck.py +3 -3
- novelwriter/core/status.py +6 -5
- novelwriter/core/storage.py +8 -2
- novelwriter/core/tohtml.py +22 -25
- novelwriter/core/tokenizer.py +417 -233
- novelwriter/core/tomd.py +17 -8
- novelwriter/core/toodt.py +386 -351
- 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 +32 -40
- novelwriter/dialogs/projectsettings.py +31 -28
- novelwriter/dialogs/quotes.py +10 -9
- novelwriter/dialogs/wordlist.py +17 -14
- novelwriter/enum.py +17 -14
- novelwriter/error.py +14 -12
- 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 +433 -496
- novelwriter/gui/dochighlight.py +54 -33
- 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 +31 -37
- novelwriter/gui/outline.py +120 -98
- novelwriter/gui/projtree.py +61 -67
- novelwriter/gui/search.py +362 -0
- novelwriter/gui/sidebar.py +36 -45
- novelwriter/gui/statusbar.py +14 -14
- novelwriter/gui/theme.py +107 -32
- novelwriter/guimain.py +209 -202
- novelwriter/shared.py +31 -6
- novelwriter/text/counting.py +138 -0
- novelwriter/tools/dictionaries.py +15 -14
- novelwriter/tools/lipsum.py +20 -17
- novelwriter/tools/manusbuild.py +43 -35
- novelwriter/tools/manuscript.py +381 -104
- novelwriter/tools/manussettings.py +262 -125
- novelwriter/tools/noveldetails.py +20 -18
- novelwriter/tools/welcome.py +52 -49
- 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.4.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/WHEEL +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
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
|
novelwriter/core/project.py
CHANGED
@@ -31,7 +31,7 @@ from time import time
|
|
31
31
|
from typing import TYPE_CHECKING
|
32
32
|
from pathlib import Path
|
33
33
|
from functools import partial
|
34
|
-
from collections.abc import
|
34
|
+
from collections.abc import Iterable
|
35
35
|
|
36
36
|
from PyQt5.QtCore import QCoreApplication
|
37
37
|
|
@@ -175,12 +175,10 @@ class NWProject:
|
|
175
175
|
"""Write content to a new document after it is created. This
|
176
176
|
will not run if the file exists and is not empty.
|
177
177
|
"""
|
178
|
-
tItem
|
179
|
-
if not (tItem and tItem.isFileType()):
|
178
|
+
if not ((tItem := self._tree[tHandle]) and tItem.isFileType()):
|
180
179
|
return False
|
181
180
|
|
182
|
-
|
183
|
-
if (newDoc.readDocument() or "").strip():
|
181
|
+
if self._storage.getDocumentText(tHandle).strip():
|
184
182
|
return False
|
185
183
|
|
186
184
|
indent = "#"*minmax(hLevel, 1, 4)
|
@@ -191,7 +189,7 @@ class NWProject:
|
|
191
189
|
else:
|
192
190
|
tItem.setLayout(nwItemLayout.NOTE)
|
193
191
|
|
194
|
-
|
192
|
+
self._storage.getDocument(tHandle).writeDocument(text)
|
195
193
|
self._index.scanText(tHandle, text)
|
196
194
|
|
197
195
|
return True
|
@@ -200,21 +198,18 @@ class NWProject:
|
|
200
198
|
"""Copy content to a new document after it is created. This
|
201
199
|
will not run if the file exists and is not empty.
|
202
200
|
"""
|
203
|
-
tItem
|
204
|
-
if not (tItem and tItem.isFileType()):
|
201
|
+
if not ((tItem := self._tree[tHandle]) and tItem.isFileType()):
|
205
202
|
return False
|
206
203
|
|
207
|
-
sItem
|
208
|
-
if not (sItem and sItem.isFileType()):
|
204
|
+
if not ((sItem := self._tree[sHandle]) and sItem.isFileType()):
|
209
205
|
return False
|
210
206
|
|
211
|
-
|
212
|
-
if (newDoc.readDocument() or "").strip():
|
207
|
+
if self._storage.getDocumentText(tHandle).strip():
|
213
208
|
return False
|
214
209
|
|
215
210
|
logger.debug("Populating '%s' with text from '%s'", tHandle, sHandle)
|
216
|
-
text = self._storage.
|
217
|
-
|
211
|
+
text = self._storage.getDocumentText(sHandle)
|
212
|
+
self._storage.getDocument(tHandle).writeDocument(text)
|
218
213
|
sItem.setLayout(tItem.itemLayout)
|
219
214
|
self._index.scanText(tHandle, text)
|
220
215
|
|
@@ -517,7 +512,7 @@ class NWProject:
|
|
517
512
|
# Class Methods
|
518
513
|
##
|
519
514
|
|
520
|
-
def iterProjectItems(self) ->
|
515
|
+
def iterProjectItems(self) -> Iterable[NWItem]:
|
521
516
|
"""This function ensures that the item tree loaded is sent to
|
522
517
|
the GUI tree view in such a way that the tree can be built. That
|
523
518
|
is, the parent item must be sent before its child. In principle,
|
novelwriter/core/projectxml.py
CHANGED
@@ -449,7 +449,7 @@ class ProjectXMLReader:
|
|
449
449
|
result[xEntry.attrib["key"]] = checkString(xEntry.text, "")
|
450
450
|
return result
|
451
451
|
|
452
|
-
def _parseDictTagText(self, xItem) -> dict:
|
452
|
+
def _parseDictTagText(self, xItem: ET.Element) -> dict:
|
453
453
|
"""Parse a dictionary stored with key as the tag and the value
|
454
454
|
as the text property.
|
455
455
|
"""
|
novelwriter/core/sessions.py
CHANGED
@@ -29,7 +29,7 @@ import logging
|
|
29
29
|
from time import time
|
30
30
|
from typing import TYPE_CHECKING
|
31
31
|
from pathlib import Path
|
32
|
-
from collections.abc import
|
32
|
+
from collections.abc import Iterable
|
33
33
|
|
34
34
|
from novelwriter.error import logException
|
35
35
|
from novelwriter.common import formatTimeStamp
|
@@ -110,7 +110,7 @@ class NWSessionLog:
|
|
110
110
|
|
111
111
|
return True
|
112
112
|
|
113
|
-
def iterRecords(self) ->
|
113
|
+
def iterRecords(self) -> Iterable[dict]:
|
114
114
|
"""Iterate through all records in the log."""
|
115
115
|
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
|
116
116
|
if isinstance(sessFile, Path) and sessFile.is_file():
|
novelwriter/core/spellcheck.py
CHANGED
@@ -74,7 +74,7 @@ class NWSpellEnchant:
|
|
74
74
|
# Setters
|
75
75
|
##
|
76
76
|
|
77
|
-
def setLanguage(self, language: str | None):
|
77
|
+
def setLanguage(self, language: str | None) -> None:
|
78
78
|
"""Load a dictionary for the language specified in the config.
|
79
79
|
If that fails, we load a mock dictionary so that lookups don't
|
80
80
|
crash. Note that enchant will allow loading an empty string as
|
@@ -182,10 +182,10 @@ class FakeEnchant:
|
|
182
182
|
def check(self, word: str) -> bool:
|
183
183
|
return True
|
184
184
|
|
185
|
-
def suggest(self, word) -> list[str]:
|
185
|
+
def suggest(self, word: str) -> list[str]:
|
186
186
|
return []
|
187
187
|
|
188
|
-
def add_to_session(self, word: str):
|
188
|
+
def add_to_session(self, word: str) -> None:
|
189
189
|
return
|
190
190
|
|
191
191
|
# END Class FakeEnchant
|
novelwriter/core/status.py
CHANGED
@@ -28,13 +28,14 @@ import random
|
|
28
28
|
import logging
|
29
29
|
|
30
30
|
from typing import TYPE_CHECKING, Literal
|
31
|
-
from collections.abc import ItemsView, Iterator, KeysView, ValuesView
|
31
|
+
from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView
|
32
32
|
|
33
33
|
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
34
|
-
from PyQt5.QtCore import QRectF
|
34
|
+
from PyQt5.QtCore import QRectF
|
35
35
|
|
36
36
|
from novelwriter import CONFIG
|
37
37
|
from novelwriter.common import minmax, simplified
|
38
|
+
from novelwriter.types import QtPaintAnitAlias, QtTransparent
|
38
39
|
|
39
40
|
if TYPE_CHECKING: # pragma: no cover
|
40
41
|
from typing import TypeGuard # Requires Python 3.10
|
@@ -193,7 +194,7 @@ class NWStatus:
|
|
193
194
|
self._store[key]["count"] += 1
|
194
195
|
return
|
195
196
|
|
196
|
-
def pack(self) ->
|
197
|
+
def pack(self) -> Iterable[tuple[str, dict]]:
|
197
198
|
"""Pack the status entries into a dictionary."""
|
198
199
|
for key, data in self._store.items():
|
199
200
|
yield (data["name"], {
|
@@ -248,10 +249,10 @@ class NWStatus:
|
|
248
249
|
def _createIcon(self, red: int, green: int, blue: int) -> QIcon:
|
249
250
|
"""Generate an icon for a status label."""
|
250
251
|
pixmap = QPixmap(self._iPX, self._iPX)
|
251
|
-
pixmap.fill(
|
252
|
+
pixmap.fill(QtTransparent)
|
252
253
|
|
253
254
|
painter = QPainter(pixmap)
|
254
|
-
painter.setRenderHint(
|
255
|
+
painter.setRenderHint(QtPaintAnitAlias)
|
255
256
|
painter.fillPath(self._iconPath, QColor(red, green, blue))
|
256
257
|
painter.end()
|
257
258
|
|
novelwriter/core/storage.py
CHANGED
@@ -27,18 +27,18 @@ import json
|
|
27
27
|
import logging
|
28
28
|
|
29
29
|
from enum import Enum
|
30
|
+
from pathlib import Path
|
30
31
|
from time import time
|
31
32
|
from typing import TYPE_CHECKING
|
32
|
-
from pathlib import Path
|
33
33
|
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
34
34
|
|
35
35
|
from novelwriter import CONFIG
|
36
|
-
from novelwriter.error import logException
|
37
36
|
from novelwriter.common import isHandle, minmax
|
38
37
|
from novelwriter.constants import nwFiles
|
39
38
|
from novelwriter.core.document import NWDocument
|
40
39
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
|
41
40
|
from novelwriter.core.spellcheck import UserDictionary
|
41
|
+
from novelwriter.error import logException
|
42
42
|
|
43
43
|
if TYPE_CHECKING: # pragma: no cover
|
44
44
|
from novelwriter.core.project import NWProject
|
@@ -287,6 +287,12 @@ class NWStorage:
|
|
287
287
|
return self._runtimePath / "meta" / fileName
|
288
288
|
return None
|
289
289
|
|
290
|
+
def getDocumentText(self, tHandle: str) -> str:
|
291
|
+
"""Return the text of a document in a fast and efficient way."""
|
292
|
+
if isinstance(self._runtimePath, Path):
|
293
|
+
return NWDocument.quickReadText(self._runtimePath / "content", tHandle)
|
294
|
+
return ""
|
295
|
+
|
290
296
|
def scanContent(self) -> list[str]:
|
291
297
|
"""Scan the content folder and return the handle of all files
|
292
298
|
found in it. Files that do not match the pattern are ignored.
|
novelwriter/core/tohtml.py
CHANGED
@@ -74,15 +74,9 @@ class ToHtml(Tokenizer):
|
|
74
74
|
# Setters
|
75
75
|
##
|
76
76
|
|
77
|
-
def setPreview(self,
|
78
|
-
"""
|
79
|
-
|
80
|
-
these flags.
|
81
|
-
"""
|
82
|
-
self._genMode = self.M_PREVIEW
|
83
|
-
self._doKeywords = True
|
84
|
-
self._doComments = doComments
|
85
|
-
self._doSynopsis = doSynopsis
|
77
|
+
def setPreview(self, state: bool) -> None:
|
78
|
+
"""Set to preview generator mode."""
|
79
|
+
self._genMode = self.M_PREVIEW if state else self.M_EXPORT
|
86
80
|
return
|
87
81
|
|
88
82
|
def setStyles(self, cssStyles: bool) -> None:
|
@@ -133,6 +127,8 @@ class ToHtml(Tokenizer):
|
|
133
127
|
self.FMT_D_E: "</span>",
|
134
128
|
self.FMT_U_B: "<u>",
|
135
129
|
self.FMT_U_E: "</u>",
|
130
|
+
self.FMT_M_B: "<mark>",
|
131
|
+
self.FMT_M_E: "</mark>",
|
136
132
|
}
|
137
133
|
else:
|
138
134
|
htmlTags = { # HTML5 (for export)
|
@@ -144,6 +140,8 @@ class ToHtml(Tokenizer):
|
|
144
140
|
self.FMT_D_E: "</del>",
|
145
141
|
self.FMT_U_B: "<span style='text-decoration: underline;'>",
|
146
142
|
self.FMT_U_E: "</span>",
|
143
|
+
self.FMT_M_B: "<mark>",
|
144
|
+
self.FMT_M_E: "</mark>",
|
147
145
|
}
|
148
146
|
|
149
147
|
htmlTags[self.FMT_SUP_B] = "<sup>"
|
@@ -171,6 +169,8 @@ class ToHtml(Tokenizer):
|
|
171
169
|
pStyle = None
|
172
170
|
lines = []
|
173
171
|
|
172
|
+
tHandle = self._handle
|
173
|
+
|
174
174
|
for tType, nHead, tText, tFormat, tStyle in self._tokens:
|
175
175
|
|
176
176
|
# Replace < and > with HTML entities
|
@@ -229,8 +229,8 @@ class ToHtml(Tokenizer):
|
|
229
229
|
else:
|
230
230
|
hStyle = ""
|
231
231
|
|
232
|
-
if self.
|
233
|
-
aNm = f"<a name='T{nHead:04d}'></a>"
|
232
|
+
if self._linkHeadings and tHandle:
|
233
|
+
aNm = f"<a name='{tHandle}:T{nHead:04d}'></a>"
|
234
234
|
else:
|
235
235
|
aNm = ""
|
236
236
|
|
@@ -252,10 +252,6 @@ class ToHtml(Tokenizer):
|
|
252
252
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
253
253
|
lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
|
254
254
|
|
255
|
-
elif tType == self.T_UNNUM:
|
256
|
-
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
257
|
-
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
258
|
-
|
259
255
|
elif tType == self.T_HEAD1:
|
260
256
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
261
257
|
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
|
@@ -296,12 +292,13 @@ class ToHtml(Tokenizer):
|
|
296
292
|
lines.append(self._formatComments(tText))
|
297
293
|
|
298
294
|
elif tType == self.T_KEYWORD and self._doKeywords:
|
299
|
-
|
295
|
+
tag, text = self._formatKeywords(tText)
|
296
|
+
kClass = f" class='meta meta-{tag}'" if tag else ""
|
297
|
+
tTemp = f"<p{kClass}{hStyle}>{text}</p>\n"
|
300
298
|
lines.append(tTemp)
|
301
299
|
|
302
300
|
self._result = "".join(lines)
|
303
|
-
|
304
|
-
self._fullHTML.append(self._result)
|
301
|
+
self._fullHTML.append(self._result)
|
305
302
|
|
306
303
|
return
|
307
304
|
|
@@ -364,13 +361,12 @@ class ToHtml(Tokenizer):
|
|
364
361
|
|
365
362
|
def getStyleSheet(self) -> list[str]:
|
366
363
|
"""Generate a stylesheet for the current settings."""
|
367
|
-
styles = []
|
368
364
|
if not self._cssStyles:
|
369
|
-
return
|
365
|
+
return []
|
370
366
|
|
371
367
|
mScale = self._lineHeight/1.15
|
372
|
-
textAlign = "justify" if self._doJustify else "left"
|
373
368
|
|
369
|
+
styles = []
|
374
370
|
styles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
|
375
371
|
self._textFont, self._textSize
|
376
372
|
))
|
@@ -380,7 +376,7 @@ class ToHtml(Tokenizer):
|
|
380
376
|
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
|
381
377
|
"}}"
|
382
378
|
).format(
|
383
|
-
|
379
|
+
"justify" if self._doJustify else "left",
|
384
380
|
round(100 * self._lineHeight),
|
385
381
|
mScale * self._marginText[0],
|
386
382
|
mScale * self._marginText[1],
|
@@ -445,6 +441,7 @@ class ToHtml(Tokenizer):
|
|
445
441
|
))
|
446
442
|
|
447
443
|
styles.append("a {color: rgb(66, 113, 174);}")
|
444
|
+
styles.append("mark {background: rgb(255, 255, 166);}")
|
448
445
|
styles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
|
449
446
|
styles.append(".break {text-align: left;}")
|
450
447
|
styles.append(".synopsis {font-style: italic;}")
|
@@ -475,11 +472,11 @@ class ToHtml(Tokenizer):
|
|
475
472
|
sComm = self._localLookup("Comment")
|
476
473
|
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
|
477
474
|
|
478
|
-
def _formatKeywords(self, text: str) -> str:
|
475
|
+
def _formatKeywords(self, text: str) -> tuple[str, str]:
|
479
476
|
"""Apply HTML formatting to keywords."""
|
480
477
|
valid, bits, _ = self._project.index.scanThis("@"+text)
|
481
478
|
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
|
482
|
-
return ""
|
479
|
+
return "", ""
|
483
480
|
|
484
481
|
result = f"<span class='tags'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
485
482
|
if len(bits) > 1:
|
@@ -494,6 +491,6 @@ class ToHtml(Tokenizer):
|
|
494
491
|
else:
|
495
492
|
result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
|
496
493
|
|
497
|
-
return result
|
494
|
+
return bits[0][1:], result
|
498
495
|
|
499
496
|
# END Class ToHtml
|