novelWriter 2.4.3__py3-none-any.whl → 2.5b1__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.4.3.dist-info → novelWriter-2.5b1.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/RECORD +109 -101
  3. novelwriter/__init__.py +33 -39
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  6. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  9. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  11. novelwriter/assets/manual.pdf +0 -0
  12. novelwriter/assets/sample.zip +0 -0
  13. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  14. novelwriter/assets/syntax/default_dark.conf +32 -18
  15. novelwriter/assets/syntax/default_light.conf +24 -10
  16. novelwriter/assets/syntax/dracula.conf +44 -0
  17. novelwriter/assets/syntax/grey_dark.conf +5 -4
  18. novelwriter/assets/syntax/grey_light.conf +5 -4
  19. novelwriter/assets/syntax/light_owl.conf +7 -6
  20. novelwriter/assets/syntax/night_owl.conf +7 -6
  21. novelwriter/assets/syntax/snazzy.conf +42 -0
  22. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  23. novelwriter/assets/syntax/solarized_light.conf +4 -3
  24. novelwriter/assets/syntax/tango.conf +27 -11
  25. novelwriter/assets/syntax/tomorrow.conf +6 -5
  26. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  27. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  28. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  29. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  30. novelwriter/assets/text/credits_en.htm +4 -1
  31. novelwriter/assets/themes/cyberpunk_night.conf +2 -0
  32. novelwriter/assets/themes/default_dark.conf +1 -0
  33. novelwriter/assets/themes/default_light.conf +1 -0
  34. novelwriter/assets/themes/dracula.conf +47 -0
  35. novelwriter/assets/themes/solarized_dark.conf +1 -0
  36. novelwriter/assets/themes/solarized_light.conf +1 -0
  37. novelwriter/common.py +31 -9
  38. novelwriter/config.py +118 -84
  39. novelwriter/constants.py +40 -26
  40. novelwriter/core/buildsettings.py +63 -66
  41. novelwriter/core/coretools.py +2 -22
  42. novelwriter/core/docbuild.py +51 -40
  43. novelwriter/core/document.py +3 -5
  44. novelwriter/core/index.py +115 -45
  45. novelwriter/core/item.py +8 -19
  46. novelwriter/core/options.py +2 -4
  47. novelwriter/core/project.py +23 -57
  48. novelwriter/core/projectdata.py +1 -3
  49. novelwriter/core/projectxml.py +12 -15
  50. novelwriter/core/sessions.py +3 -5
  51. novelwriter/core/spellcheck.py +4 -9
  52. novelwriter/core/status.py +211 -164
  53. novelwriter/core/storage.py +0 -8
  54. novelwriter/core/tohtml.py +94 -100
  55. novelwriter/core/tokenizer.py +199 -112
  56. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  57. novelwriter/core/toodt.py +212 -148
  58. novelwriter/core/toqdoc.py +403 -0
  59. novelwriter/core/tree.py +5 -7
  60. novelwriter/dialogs/about.py +3 -5
  61. novelwriter/dialogs/docmerge.py +1 -3
  62. novelwriter/dialogs/docsplit.py +1 -3
  63. novelwriter/dialogs/editlabel.py +0 -2
  64. novelwriter/dialogs/preferences.py +111 -88
  65. novelwriter/dialogs/projectsettings.py +216 -180
  66. novelwriter/dialogs/quotes.py +3 -4
  67. novelwriter/dialogs/wordlist.py +3 -9
  68. novelwriter/enum.py +31 -25
  69. novelwriter/error.py +8 -15
  70. novelwriter/extensions/circularprogress.py +5 -6
  71. novelwriter/extensions/configlayout.py +18 -18
  72. novelwriter/extensions/eventfilters.py +1 -5
  73. novelwriter/extensions/modified.py +50 -13
  74. novelwriter/extensions/novelselector.py +1 -3
  75. novelwriter/extensions/pagedsidebar.py +9 -12
  76. novelwriter/extensions/simpleprogress.py +1 -3
  77. novelwriter/extensions/statusled.py +1 -3
  78. novelwriter/extensions/switch.py +4 -6
  79. novelwriter/extensions/switchbox.py +7 -6
  80. novelwriter/extensions/versioninfo.py +3 -9
  81. novelwriter/gui/doceditor.py +98 -126
  82. novelwriter/gui/dochighlight.py +237 -183
  83. novelwriter/gui/docviewer.py +46 -94
  84. novelwriter/gui/docviewerpanel.py +3 -10
  85. novelwriter/gui/editordocument.py +1 -3
  86. novelwriter/gui/itemdetails.py +7 -11
  87. novelwriter/gui/mainmenu.py +11 -7
  88. novelwriter/gui/noveltree.py +11 -24
  89. novelwriter/gui/outline.py +11 -23
  90. novelwriter/gui/projtree.py +26 -43
  91. novelwriter/gui/search.py +1 -3
  92. novelwriter/gui/sidebar.py +2 -6
  93. novelwriter/gui/statusbar.py +6 -10
  94. novelwriter/gui/theme.py +23 -48
  95. novelwriter/guimain.py +50 -71
  96. novelwriter/shared.py +30 -15
  97. novelwriter/tools/dictionaries.py +8 -12
  98. novelwriter/tools/lipsum.py +2 -4
  99. novelwriter/tools/manusbuild.py +1 -3
  100. novelwriter/tools/manuscript.py +66 -145
  101. novelwriter/tools/manussettings.py +67 -73
  102. novelwriter/tools/noveldetails.py +6 -11
  103. novelwriter/tools/welcome.py +2 -16
  104. novelwriter/tools/writingstats.py +6 -9
  105. novelwriter/types.py +45 -3
  106. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/entry_points.txt +0 -0
  109. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/top_level.txt +0 -0
@@ -26,17 +26,36 @@ from __future__ import annotations
26
26
  import json
27
27
  import logging
28
28
 
29
- from time import time
30
29
  from pathlib import Path
30
+ from time import time
31
31
 
32
32
  from novelwriter import CONFIG
33
33
  from novelwriter.common import formatTimeStamp
34
- from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode
34
+ from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
35
35
  from novelwriter.core.project import NWProject
36
- from novelwriter.core.tokenizer import Tokenizer, stripEscape
36
+ from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
37
+ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
37
38
 
38
39
  logger = logging.getLogger(__name__)
39
40
 
41
+ HTML5_TAGS = {
42
+ Tokenizer.FMT_B_B: "<strong>",
43
+ Tokenizer.FMT_B_E: "</strong>",
44
+ Tokenizer.FMT_I_B: "<em>",
45
+ Tokenizer.FMT_I_E: "</em>",
46
+ Tokenizer.FMT_D_B: "<del>",
47
+ Tokenizer.FMT_D_E: "</del>",
48
+ Tokenizer.FMT_U_B: "<span style='text-decoration: underline;'>",
49
+ Tokenizer.FMT_U_E: "</span>",
50
+ Tokenizer.FMT_M_B: "<mark>",
51
+ Tokenizer.FMT_M_E: "</mark>",
52
+ Tokenizer.FMT_SUP_B: "<sup>",
53
+ Tokenizer.FMT_SUP_E: "</sup>",
54
+ Tokenizer.FMT_SUB_B: "<sub>",
55
+ Tokenizer.FMT_SUB_E: "</sub>",
56
+ Tokenizer.FMT_STRIP: "",
57
+ }
58
+
40
59
 
41
60
  class ToHtml(Tokenizer):
42
61
  """Core: HTML Document Writer
@@ -45,19 +64,15 @@ class ToHtml(Tokenizer):
45
64
  also used by the Document Viewer, and Manuscript Build Preview.
46
65
  """
47
66
 
48
- M_PREVIEW = 0 # Tweak output for the DocViewer
49
- M_EXPORT = 1 # Tweak output for saving to HTML or printing
50
- M_EBOOK = 2 # Tweak output for converting to epub
51
-
52
67
  def __init__(self, project: NWProject) -> None:
53
68
  super().__init__(project)
54
69
 
55
- self._genMode = self.M_EXPORT
56
70
  self._cssStyles = True
57
71
  self._fullHTML: list[str] = []
58
72
 
59
73
  # Internals
60
74
  self._trMap = {}
75
+ self._usedNotes: dict[str, int] = {}
61
76
  self.setReplaceUnicode(False)
62
77
 
63
78
  return
@@ -74,11 +89,6 @@ class ToHtml(Tokenizer):
74
89
  # Setters
75
90
  ##
76
91
 
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
80
- return
81
-
82
92
  def setStyles(self, cssStyles: bool) -> None:
83
93
  """Enable or disable CSS styling. Some elements may still have
84
94
  class tags.
@@ -117,39 +127,9 @@ class ToHtml(Tokenizer):
117
127
 
118
128
  def doConvert(self) -> None:
119
129
  """Convert the list of text tokens into an HTML document."""
120
- if self._genMode == self.M_PREVIEW:
121
- htmlTags = { # HTML4 + CSS2 (for Qt)
122
- self.FMT_B_B: "<b>",
123
- self.FMT_B_E: "</b>",
124
- self.FMT_I_B: "<i>",
125
- self.FMT_I_E: "</i>",
126
- self.FMT_D_B: "<span style='text-decoration: line-through;'>",
127
- self.FMT_D_E: "</span>",
128
- self.FMT_U_B: "<u>",
129
- self.FMT_U_E: "</u>",
130
- self.FMT_M_B: "<mark>",
131
- self.FMT_M_E: "</mark>",
132
- }
133
- else:
134
- htmlTags = { # HTML5 (for export)
135
- self.FMT_B_B: "<strong>",
136
- self.FMT_B_E: "</strong>",
137
- self.FMT_I_B: "<em>",
138
- self.FMT_I_E: "</em>",
139
- self.FMT_D_B: "<del>",
140
- self.FMT_D_E: "</del>",
141
- self.FMT_U_B: "<span style='text-decoration: underline;'>",
142
- self.FMT_U_E: "</span>",
143
- self.FMT_M_B: "<mark>",
144
- self.FMT_M_E: "</mark>",
145
- }
146
-
147
- htmlTags[self.FMT_SUP_B] = "<sup>"
148
- htmlTags[self.FMT_SUP_E] = "</sup>"
149
- htmlTags[self.FMT_SUB_B] = "<sub>"
150
- htmlTags[self.FMT_SUB_E] = "</sub>"
130
+ self._result = ""
151
131
 
152
- if self._isNovel and self._genMode != self.M_PREVIEW:
132
+ if self._isNovel:
153
133
  # For story files, we bump the titles one level up
154
134
  h1Cl = " class='title'"
155
135
  h1 = "h1"
@@ -163,12 +143,7 @@ class ToHtml(Tokenizer):
163
143
  h3 = "h3"
164
144
  h4 = "h4"
165
145
 
166
- self._result = ""
167
-
168
- para = []
169
- pStyle = None
170
146
  lines = []
171
-
172
147
  tHandle = self._handle
173
148
 
174
149
  for tType, nHead, tText, tFormat, tStyle in self._tokens:
@@ -181,11 +156,11 @@ class ToHtml(Tokenizer):
181
156
  for c in tText:
182
157
  if c == "<":
183
158
  cText.append("&lt;")
184
- tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
159
+ tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
185
160
  i += 4
186
161
  elif c == ">":
187
162
  cText.append("&gt;")
188
- tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
163
+ tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
189
164
  i += 4
190
165
  else:
191
166
  cText.append(c)
@@ -235,37 +210,27 @@ class ToHtml(Tokenizer):
235
210
  aNm = ""
236
211
 
237
212
  # Process Text Type
238
- if tType == self.T_EMPTY:
239
- if pStyle is None:
240
- pStyle = ""
241
- if len(para) > 1 and self._cssStyles:
242
- pClass = " class='break'"
243
- else:
244
- pClass = ""
245
- if len(para) > 0:
246
- tTemp = "<br/>".join(para)
247
- lines.append(f"<p{pClass+pStyle}>{tTemp.rstrip()}</p>\n")
248
- para = []
249
- pStyle = None
213
+ if tType == self.T_TEXT:
214
+ lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
250
215
 
251
216
  elif tType == self.T_TITLE:
252
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
217
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
253
218
  lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
254
219
 
255
220
  elif tType == self.T_HEAD1:
256
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
221
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
257
222
  lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
258
223
 
259
224
  elif tType == self.T_HEAD2:
260
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
225
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
261
226
  lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
262
227
 
263
228
  elif tType == self.T_HEAD3:
264
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
229
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
265
230
  lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
266
231
 
267
232
  elif tType == self.T_HEAD4:
268
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
233
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
269
234
  lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
270
235
 
271
236
  elif tType == self.T_SEP:
@@ -274,22 +239,14 @@ class ToHtml(Tokenizer):
274
239
  elif tType == self.T_SKIP:
275
240
  lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")
276
241
 
277
- elif tType == self.T_TEXT:
278
- tTemp = tText
279
- if pStyle is None:
280
- pStyle = hStyle
281
- for pos, fmt in reversed(tFormat):
282
- tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
283
- para.append(stripEscape(tTemp.rstrip()))
284
-
285
242
  elif tType == self.T_SYNOPSIS and self._doSynopsis:
286
- lines.append(self._formatSynopsis(tText, True))
243
+ lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
287
244
 
288
245
  elif tType == self.T_SHORT and self._doSynopsis:
289
- lines.append(self._formatSynopsis(tText, False))
246
+ lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
290
247
 
291
248
  elif tType == self.T_COMMENT and self._doComments:
292
- lines.append(self._formatComments(tText))
249
+ lines.append(self._formatComments(self._formatText(tText, tFormat)))
293
250
 
294
251
  elif tType == self.T_KEYWORD and self._doKeywords:
295
252
  tag, text = self._formatKeywords(tText)
@@ -302,6 +259,26 @@ class ToHtml(Tokenizer):
302
259
 
303
260
  return
304
261
 
262
+ def appendFootnotes(self) -> None:
263
+ """Append the footnotes in the buffer."""
264
+ if self._usedNotes:
265
+ footnotes = self._localLookup("Footnotes")
266
+
267
+ lines = []
268
+ lines.append(f"<h3>{footnotes}</h3>\n")
269
+ lines.append("<ol>\n")
270
+ for key, index in self._usedNotes.items():
271
+ if content := self._footnotes.get(key):
272
+ text = self._formatText(*content)
273
+ lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
274
+ lines.append("</ol>\n")
275
+
276
+ result = "".join(lines)
277
+ self._result += result
278
+ self._fullHTML.append(result)
279
+
280
+ return
281
+
305
282
  def saveHtml5(self, path: str | Path) -> None:
306
283
  """Save the data to an HTML file."""
307
284
  with open(path, mode="w", encoding="utf-8") as fObj:
@@ -367,8 +344,16 @@ class ToHtml(Tokenizer):
367
344
  mScale = self._lineHeight/1.15
368
345
 
369
346
  styles = []
370
- styles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
371
- self._textFont, self._textSize
347
+ font = self._textFont
348
+ styles.append((
349
+ "body {{"
350
+ "font-family: '{0:s}'; font-size: {1:d}pt; "
351
+ "font-weight: {2:d}; font-style: {3:s};"
352
+ "}}"
353
+ ).format(
354
+ font.family(), font.pointSize(),
355
+ FONT_WEIGHTS.get(font.weight(), 400),
356
+ FONT_STYLE.get(font.style(), "normal"),
372
357
  ))
373
358
  styles.append((
374
359
  "p {{"
@@ -442,7 +427,7 @@ class ToHtml(Tokenizer):
442
427
 
443
428
  styles.append("a {color: rgb(66, 113, 174);}")
444
429
  styles.append("mark {background: rgb(255, 255, 166);}")
445
- styles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
430
+ styles.append(".keyword {color: rgb(245, 135, 31); font-weight: bold;}")
446
431
  styles.append(".break {text-align: left;}")
447
432
  styles.append(".synopsis {font-style: italic;}")
448
433
  styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
@@ -453,24 +438,36 @@ class ToHtml(Tokenizer):
453
438
  # Internal Functions
454
439
  ##
455
440
 
441
+ def _formatText(self, text: str, tFmt: T_Formats) -> str:
442
+ """Apply formatting tags to text."""
443
+ temp = text
444
+ for pos, fmt, data in reversed(tFmt):
445
+ html = ""
446
+ if fmt == self.FMT_FNOTE:
447
+ if data in self._footnotes:
448
+ index = len(self._usedNotes) + 1
449
+ self._usedNotes[data] = index
450
+ html = f"<sup><a href='#footnote_{index}'>{index}</a></sup>"
451
+ else:
452
+ html = "<sup>ERR</sup>"
453
+ else:
454
+ html = HTML5_TAGS.get(fmt, "ERR")
455
+ temp = f"{temp[:pos]}{html}{temp[pos:]}"
456
+ temp = temp.replace("\n", "<br>")
457
+ return stripEscape(temp)
458
+
456
459
  def _formatSynopsis(self, text: str, synopsis: bool) -> str:
457
460
  """Apply HTML formatting to synopsis."""
458
461
  if synopsis:
459
462
  sSynop = self._localLookup("Synopsis")
460
463
  else:
461
464
  sSynop = self._localLookup("Short Description")
462
- if self._genMode == self.M_PREVIEW:
463
- return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
464
- else:
465
- return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
465
+ return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
466
466
 
467
467
  def _formatComments(self, text: str) -> str:
468
468
  """Apply HTML formatting to comments."""
469
- if self._genMode == self.M_PREVIEW:
470
- return f"<p class='comment'>{text}</p>\n"
471
- else:
472
- sComm = self._localLookup("Comment")
473
- return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
469
+ sComm = self._localLookup("Comment")
470
+ return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
474
471
 
475
472
  def _formatKeywords(self, text: str) -> tuple[str, str]:
476
473
  """Apply HTML formatting to keywords."""
@@ -478,19 +475,16 @@ class ToHtml(Tokenizer):
478
475
  if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
479
476
  return "", ""
480
477
 
481
- result = f"<span class='tags'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
478
+ result = f"<span class='keyword'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
482
479
  if len(bits) > 1:
483
480
  if bits[0] == nwKeyWords.TAG_KEY:
484
481
  one, two = self._project.index.parseValue(bits[1])
485
- result += f"<a name='tag_{one}'>{one}</a>"
482
+ result += f"<a class='tag' name='tag_{one}'>{one}</a>"
486
483
  if two:
487
484
  result += f" | <span class='optional'>{two}</a>"
488
485
  else:
489
- if self._genMode == self.M_PREVIEW:
490
- result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
491
- else:
492
- result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
486
+ result += ", ".join(
487
+ f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
488
+ )
493
489
 
494
490
  return bits[0][1:], result
495
-
496
- # END Class ToHtml