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.
Files changed (100) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +99 -85
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4b1.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +5 -5
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  15. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  16. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  18. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  21. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  24. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  25. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  26. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  29. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  32. novelwriter/assets/manual.pdf +0 -0
  33. novelwriter/assets/sample.zip +0 -0
  34. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  35. novelwriter/assets/syntax/default_dark.conf +1 -0
  36. novelwriter/assets/syntax/default_light.conf +1 -0
  37. novelwriter/assets/syntax/grey_dark.conf +1 -0
  38. novelwriter/assets/syntax/grey_light.conf +1 -0
  39. novelwriter/assets/syntax/light_owl.conf +1 -0
  40. novelwriter/assets/syntax/night_owl.conf +1 -0
  41. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  42. novelwriter/assets/syntax/solarized_light.conf +1 -0
  43. novelwriter/assets/syntax/tango.conf +23 -0
  44. novelwriter/assets/syntax/tomorrow.conf +1 -0
  45. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  46. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  47. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  49. novelwriter/assets/text/credits_en.htm +25 -23
  50. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  51. novelwriter/common.py +1 -1
  52. novelwriter/config.py +35 -12
  53. novelwriter/constants.py +5 -6
  54. novelwriter/core/buildsettings.py +60 -40
  55. novelwriter/core/coretools.py +98 -13
  56. novelwriter/core/docbuild.py +74 -7
  57. novelwriter/core/document.py +24 -3
  58. novelwriter/core/index.py +31 -112
  59. novelwriter/core/project.py +11 -15
  60. novelwriter/core/projectxml.py +2 -1
  61. novelwriter/core/sessions.py +2 -2
  62. novelwriter/core/status.py +4 -4
  63. novelwriter/core/storage.py +16 -6
  64. novelwriter/core/tohtml.py +22 -25
  65. novelwriter/core/tokenizer.py +416 -236
  66. novelwriter/core/tomd.py +17 -8
  67. novelwriter/core/toodt.py +65 -7
  68. novelwriter/core/tree.py +8 -8
  69. novelwriter/dialogs/about.py +2 -2
  70. novelwriter/dialogs/docsplit.py +7 -8
  71. novelwriter/dialogs/preferences.py +3 -6
  72. novelwriter/dialogs/wordlist.py +1 -1
  73. novelwriter/enum.py +17 -14
  74. novelwriter/extensions/configlayout.py +22 -0
  75. novelwriter/extensions/modified.py +20 -2
  76. novelwriter/extensions/versioninfo.py +1 -1
  77. novelwriter/gui/doceditor.py +257 -279
  78. novelwriter/gui/dochighlight.py +29 -25
  79. novelwriter/gui/docviewer.py +139 -148
  80. novelwriter/gui/docviewerpanel.py +4 -24
  81. novelwriter/gui/editordocument.py +12 -1
  82. novelwriter/gui/itemdetails.py +6 -6
  83. novelwriter/gui/mainmenu.py +37 -17
  84. novelwriter/gui/noveltree.py +11 -19
  85. novelwriter/gui/outline.py +43 -20
  86. novelwriter/gui/projtree.py +88 -88
  87. novelwriter/gui/search.py +316 -0
  88. novelwriter/gui/sidebar.py +25 -30
  89. novelwriter/gui/theme.py +68 -8
  90. novelwriter/guimain.py +183 -178
  91. novelwriter/shared.py +26 -1
  92. novelwriter/text/__init__.py +3 -0
  93. novelwriter/text/counting.py +137 -0
  94. novelwriter/tools/manuscript.py +344 -55
  95. novelwriter/tools/manussettings.py +214 -71
  96. novelwriter/tools/noveldetails.py +1 -1
  97. novelwriter/tools/welcome.py +8 -9
  98. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  99. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
  100. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
@@ -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 Iterator
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 = self._tree[tHandle]
179
- if not (tItem and tItem.isFileType()):
178
+ if not ((tItem := self._tree[tHandle]) and tItem.isFileType()):
180
179
  return False
181
180
 
182
- newDoc = self._storage.getDocument(tHandle)
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
- newDoc.writeDocument(text)
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 = self._tree[tHandle]
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 = self._tree[sHandle]
208
- if not (sItem and sItem.isFileType()):
204
+ if not ((sItem := self._tree[sHandle]) and sItem.isFileType()):
209
205
  return False
210
206
 
211
- newDoc = self._storage.getDocument(tHandle)
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.getDocument(sHandle).readDocument() or ""
217
- newDoc.writeDocument(text)
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) -> Iterator[NWItem]:
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,
@@ -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 = "2" # The current project file format 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:
@@ -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 Iterator
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) -> Iterator[dict]:
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():
@@ -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, Qt
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) -> Iterator[tuple[str, dict]]:
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(Qt.transparent)
251
+ pixmap.fill(QColor(0, 0, 0, 0))
252
252
 
253
253
  painter = QPainter(pixmap)
254
254
  painter.setRenderHint(QPainter.Antialiasing)
@@ -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() and inPath.name == nwFiles.PROJ_FILE:
187
- nwxFile = inPath
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 a novelWriter project")
190
- return NWStorageOpen.UNKOWN
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.
@@ -74,15 +74,9 @@ class ToHtml(Tokenizer):
74
74
  # Setters
75
75
  ##
76
76
 
77
- def setPreview(self, doComments: bool, doSynopsis: bool) -> None:
78
- """If we're using this class to generate markdown preview, we
79
- need to make a few changes to formatting, which is managed by
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._linkHeaders:
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
- tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
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
- if self._genMode != self.M_PREVIEW:
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 styles
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
- textAlign,
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