novelWriter 2.3rc1__py3-none-any.whl → 2.4b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +99 -85
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +1 -1
- novelWriter-2.4b1.dist-info/entry_points.txt +2 -0
- novelwriter/__init__.py +5 -5
- 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/project_nl_NL.json +11 -0
- novelwriter/assets/i18n/project_pt_BR.json +11 -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_arrow-down.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -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_light/icons.conf +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -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/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/cyberpunk_night.conf +26 -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/tango.conf +23 -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/assets/themes/cyberpunk_night.conf +29 -0
- novelwriter/common.py +1 -1
- novelwriter/config.py +35 -12
- novelwriter/constants.py +5 -6
- novelwriter/core/buildsettings.py +60 -40
- novelwriter/core/coretools.py +98 -13
- novelwriter/core/docbuild.py +74 -7
- novelwriter/core/document.py +24 -3
- novelwriter/core/index.py +31 -112
- novelwriter/core/project.py +11 -15
- novelwriter/core/projectxml.py +2 -1
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/status.py +4 -4
- novelwriter/core/storage.py +16 -6
- novelwriter/core/tohtml.py +22 -25
- novelwriter/core/tokenizer.py +416 -236
- novelwriter/core/tomd.py +17 -8
- novelwriter/core/toodt.py +65 -7
- novelwriter/core/tree.py +8 -8
- novelwriter/dialogs/about.py +2 -2
- novelwriter/dialogs/docsplit.py +7 -8
- novelwriter/dialogs/preferences.py +3 -6
- novelwriter/dialogs/wordlist.py +1 -1
- novelwriter/enum.py +17 -14
- novelwriter/extensions/configlayout.py +22 -0
- novelwriter/extensions/modified.py +20 -2
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/gui/doceditor.py +257 -279
- novelwriter/gui/dochighlight.py +29 -25
- novelwriter/gui/docviewer.py +139 -148
- novelwriter/gui/docviewerpanel.py +4 -24
- novelwriter/gui/editordocument.py +12 -1
- novelwriter/gui/itemdetails.py +6 -6
- novelwriter/gui/mainmenu.py +37 -17
- novelwriter/gui/noveltree.py +11 -19
- novelwriter/gui/outline.py +43 -20
- novelwriter/gui/projtree.py +88 -88
- novelwriter/gui/search.py +316 -0
- novelwriter/gui/sidebar.py +25 -30
- novelwriter/gui/theme.py +68 -8
- novelwriter/guimain.py +183 -178
- novelwriter/shared.py +26 -1
- novelwriter/text/__init__.py +3 -0
- novelwriter/text/counting.py +137 -0
- novelwriter/tools/manuscript.py +344 -55
- novelwriter/tools/manussettings.py +214 -71
- novelwriter/tools/noveldetails.py +1 -1
- novelwriter/tools/welcome.py +8 -9
- novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
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
|
|
@@ -410,6 +405,7 @@ class NWProject:
|
|
410
405
|
def closeProject(self, idleTime: float = 0.0) -> None:
|
411
406
|
"""Close the project."""
|
412
407
|
logger.info("Closing project")
|
408
|
+
self._index.clearIndex() # Triggers clear signal, see #1718
|
413
409
|
self._options.saveSettings()
|
414
410
|
self._tree.writeToCFile()
|
415
411
|
self._session.appendSession(idleTime)
|
@@ -516,7 +512,7 @@ class NWProject:
|
|
516
512
|
# Class Methods
|
517
513
|
##
|
518
514
|
|
519
|
-
def iterProjectItems(self) ->
|
515
|
+
def iterProjectItems(self) -> Iterable[NWItem]:
|
520
516
|
"""This function ensures that the item tree loaded is sent to
|
521
517
|
the GUI tree view in such a way that the tree can be built. That
|
522
518
|
is, the parent item must be sent before its child. In principle,
|
novelwriter/core/projectxml.py
CHANGED
@@ -46,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
46
46
|
logger = logging.getLogger(__name__)
|
47
47
|
|
48
48
|
FILE_VERSION = "1.5" # The current project file format version
|
49
|
-
FILE_REVISION = "
|
49
|
+
FILE_REVISION = "3" # The current project file format revision
|
50
50
|
HEX_VERSION = 0x0105
|
51
51
|
|
52
52
|
NUM_VERSION = {
|
@@ -108,6 +108,7 @@ class ProjectXMLReader:
|
|
108
108
|
Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1.
|
109
109
|
Rev 2: Drops the title node from project and adds the TEMPLATE
|
110
110
|
class for items. 2.3 Beta 1.
|
111
|
+
Rev 3: Added TEMPLATE class. 2.3.
|
111
112
|
"""
|
112
113
|
|
113
114
|
def __init__(self, path: str | Path) -> None:
|
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/status.py
CHANGED
@@ -28,10 +28,10 @@ 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
|
@@ -193,7 +193,7 @@ class NWStatus:
|
|
193
193
|
self._store[key]["count"] += 1
|
194
194
|
return
|
195
195
|
|
196
|
-
def pack(self) ->
|
196
|
+
def pack(self) -> Iterable[tuple[str, dict]]:
|
197
197
|
"""Pack the status entries into a dictionary."""
|
198
198
|
for key, data in self._store.items():
|
199
199
|
yield (data["name"], {
|
@@ -248,7 +248,7 @@ class NWStatus:
|
|
248
248
|
def _createIcon(self, red: int, green: int, blue: int) -> QIcon:
|
249
249
|
"""Generate an icon for a status label."""
|
250
250
|
pixmap = QPixmap(self._iPX, self._iPX)
|
251
|
-
pixmap.fill(
|
251
|
+
pixmap.fill(QColor(0, 0, 0, 0))
|
252
252
|
|
253
253
|
painter = QPainter(pixmap)
|
254
254
|
painter.setRenderHint(QPainter.Antialiasing)
|
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
|
@@ -183,11 +183,15 @@ class NWStorage:
|
|
183
183
|
# 2. A full path to an nwProject.nwx file
|
184
184
|
if inPath.is_dir() and inPath != Path.home().resolve():
|
185
185
|
nwxFile = inPath / nwFiles.PROJ_FILE
|
186
|
-
elif inPath.is_file()
|
187
|
-
|
186
|
+
elif inPath.is_file():
|
187
|
+
if inPath.name == nwFiles.PROJ_FILE:
|
188
|
+
nwxFile = inPath
|
189
|
+
else:
|
190
|
+
logger.error("Not a novelWriter project")
|
191
|
+
return NWStorageOpen.UNKOWN
|
188
192
|
else:
|
189
|
-
logger.error("Not
|
190
|
-
return NWStorageOpen.
|
193
|
+
logger.error("Not found: %s", inPath)
|
194
|
+
return NWStorageOpen.NOT_FOUND
|
191
195
|
|
192
196
|
if not nwxFile.exists():
|
193
197
|
# The .nwx file must exist to continue
|
@@ -283,6 +287,12 @@ class NWStorage:
|
|
283
287
|
return self._runtimePath / "meta" / fileName
|
284
288
|
return None
|
285
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
|
+
|
286
296
|
def scanContent(self) -> list[str]:
|
287
297
|
"""Scan the content folder and return the handle of all files
|
288
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
|