novelWriter 2.4.4__py3-none-any.whl → 2.5__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 (122) hide show
  1. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/RECORD +121 -111
  3. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +33 -39
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_en_GB.json +1 -0
  17. novelwriter/assets/i18n/project_pl_PL.json +116 -0
  18. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  19. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  21. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  22. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  23. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  24. novelwriter/assets/manual.pdf +0 -0
  25. novelwriter/assets/sample.zip +0 -0
  26. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  27. novelwriter/assets/syntax/default_dark.conf +32 -18
  28. novelwriter/assets/syntax/default_light.conf +24 -10
  29. novelwriter/assets/syntax/dracula.conf +44 -0
  30. novelwriter/assets/syntax/grey_dark.conf +5 -4
  31. novelwriter/assets/syntax/grey_light.conf +5 -4
  32. novelwriter/assets/syntax/light_owl.conf +7 -6
  33. novelwriter/assets/syntax/night_owl.conf +7 -6
  34. novelwriter/assets/syntax/snazzy.conf +42 -0
  35. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  36. novelwriter/assets/syntax/solarized_light.conf +4 -3
  37. novelwriter/assets/syntax/tango.conf +27 -11
  38. novelwriter/assets/syntax/tomorrow.conf +6 -5
  39. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  40. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  41. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  42. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  43. novelwriter/assets/text/credits_en.htm +52 -41
  44. novelwriter/assets/themes/cyberpunk_night.conf +3 -0
  45. novelwriter/assets/themes/default_dark.conf +2 -0
  46. novelwriter/assets/themes/default_light.conf +2 -0
  47. novelwriter/assets/themes/dracula.conf +48 -0
  48. novelwriter/assets/themes/solarized_dark.conf +2 -0
  49. novelwriter/assets/themes/solarized_light.conf +2 -0
  50. novelwriter/common.py +33 -12
  51. novelwriter/config.py +184 -98
  52. novelwriter/constants.py +47 -35
  53. novelwriter/core/buildsettings.py +68 -69
  54. novelwriter/core/coretools.py +5 -23
  55. novelwriter/core/docbuild.py +52 -40
  56. novelwriter/core/document.py +3 -5
  57. novelwriter/core/index.py +115 -45
  58. novelwriter/core/item.py +8 -19
  59. novelwriter/core/options.py +2 -4
  60. novelwriter/core/project.py +37 -61
  61. novelwriter/core/projectdata.py +1 -3
  62. novelwriter/core/projectxml.py +12 -15
  63. novelwriter/core/sessions.py +3 -5
  64. novelwriter/core/spellcheck.py +4 -9
  65. novelwriter/core/status.py +211 -164
  66. novelwriter/core/storage.py +0 -8
  67. novelwriter/core/tohtml.py +139 -105
  68. novelwriter/core/tokenizer.py +278 -122
  69. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  70. novelwriter/core/toodt.py +257 -166
  71. novelwriter/core/toqdoc.py +419 -0
  72. novelwriter/core/tree.py +5 -7
  73. novelwriter/dialogs/about.py +11 -18
  74. novelwriter/dialogs/docmerge.py +17 -19
  75. novelwriter/dialogs/docsplit.py +17 -19
  76. novelwriter/dialogs/editlabel.py +6 -10
  77. novelwriter/dialogs/preferences.py +200 -164
  78. novelwriter/dialogs/projectsettings.py +225 -189
  79. novelwriter/dialogs/quotes.py +12 -9
  80. novelwriter/dialogs/wordlist.py +9 -15
  81. novelwriter/enum.py +35 -30
  82. novelwriter/error.py +8 -15
  83. novelwriter/extensions/configlayout.py +55 -21
  84. novelwriter/extensions/eventfilters.py +1 -5
  85. novelwriter/extensions/modified.py +58 -14
  86. novelwriter/extensions/novelselector.py +1 -3
  87. novelwriter/extensions/pagedsidebar.py +9 -12
  88. novelwriter/extensions/{circularprogress.py → progressbars.py} +30 -8
  89. novelwriter/extensions/statusled.py +40 -26
  90. novelwriter/extensions/switch.py +4 -6
  91. novelwriter/extensions/switchbox.py +7 -6
  92. novelwriter/extensions/versioninfo.py +3 -9
  93. novelwriter/gui/doceditor.py +120 -139
  94. novelwriter/gui/dochighlight.py +231 -186
  95. novelwriter/gui/docviewer.py +69 -108
  96. novelwriter/gui/docviewerpanel.py +3 -10
  97. novelwriter/gui/editordocument.py +1 -3
  98. novelwriter/gui/itemdetails.py +7 -11
  99. novelwriter/gui/mainmenu.py +22 -18
  100. novelwriter/gui/noveltree.py +11 -24
  101. novelwriter/gui/outline.py +15 -26
  102. novelwriter/gui/projtree.py +35 -60
  103. novelwriter/gui/search.py +10 -3
  104. novelwriter/gui/sidebar.py +2 -6
  105. novelwriter/gui/statusbar.py +29 -37
  106. novelwriter/gui/theme.py +26 -48
  107. novelwriter/guimain.py +162 -160
  108. novelwriter/shared.py +36 -32
  109. novelwriter/text/patterns.py +113 -0
  110. novelwriter/tools/dictionaries.py +10 -20
  111. novelwriter/tools/lipsum.py +10 -16
  112. novelwriter/tools/manusbuild.py +9 -11
  113. novelwriter/tools/manuscript.py +71 -145
  114. novelwriter/tools/manussettings.py +71 -75
  115. novelwriter/tools/noveldetails.py +16 -21
  116. novelwriter/tools/welcome.py +21 -26
  117. novelwriter/tools/writingstats.py +9 -12
  118. novelwriter/types.py +49 -4
  119. novelwriter/extensions/simpleprogress.py +0 -55
  120. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/LICENSE.md +0 -0
  121. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/entry_points.txt +0 -0
  122. {novelWriter-2.4.4.dist-info → novelWriter-2.5.dist-info}/top_level.txt +0 -0
@@ -26,17 +26,46 @@ 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
- from novelwriter import CONFIG
33
32
  from novelwriter.common import formatTimeStamp
34
- from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode
33
+ from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
35
34
  from novelwriter.core.project import NWProject
36
- from novelwriter.core.tokenizer import Tokenizer, stripEscape
35
+ from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
36
+ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
37
37
 
38
38
  logger = logging.getLogger(__name__)
39
39
 
40
+ # Each opener tag, with the id of its corresponding closer and tag format
41
+ HTML_OPENER: dict[int, tuple[int, str]] = {
42
+ Tokenizer.FMT_B_B: (Tokenizer.FMT_B_E, "<strong>"),
43
+ Tokenizer.FMT_I_B: (Tokenizer.FMT_I_E, "<em>"),
44
+ Tokenizer.FMT_D_B: (Tokenizer.FMT_D_E, "<del>"),
45
+ Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, "<span style='text-decoration: underline;'>"),
46
+ Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, "<mark>"),
47
+ Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, "<sup>"),
48
+ Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, "<sub>"),
49
+ Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, "<span class='dialog'>"),
50
+ Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, "<span class='altdialog'>"),
51
+ }
52
+
53
+ # Each closer tag, with the id of its corresponding opener and tag format
54
+ HTML_CLOSER: dict[int, tuple[int, str]] = {
55
+ Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, "</strong>"),
56
+ Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, "</em>"),
57
+ Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, "</del>"),
58
+ Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, "</span>"),
59
+ Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, "</mark>"),
60
+ Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, "</sup>"),
61
+ Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, "</sub>"),
62
+ Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, "</span>"),
63
+ Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, "</span>"),
64
+ }
65
+
66
+ # Empty HTML tag record
67
+ HTML_NONE = (0, "")
68
+
40
69
 
41
70
  class ToHtml(Tokenizer):
42
71
  """Core: HTML Document Writer
@@ -45,19 +74,15 @@ class ToHtml(Tokenizer):
45
74
  also used by the Document Viewer, and Manuscript Build Preview.
46
75
  """
47
76
 
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
77
  def __init__(self, project: NWProject) -> None:
53
78
  super().__init__(project)
54
79
 
55
- self._genMode = self.M_EXPORT
56
80
  self._cssStyles = True
57
81
  self._fullHTML: list[str] = []
58
82
 
59
83
  # Internals
60
84
  self._trMap = {}
85
+ self._usedNotes: dict[str, int] = {}
61
86
  self.setReplaceUnicode(False)
62
87
 
63
88
  return
@@ -74,11 +99,6 @@ class ToHtml(Tokenizer):
74
99
  # Setters
75
100
  ##
76
101
 
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
102
  def setStyles(self, cssStyles: bool) -> None:
83
103
  """Enable or disable CSS styling. Some elements may still have
84
104
  class tags.
@@ -117,39 +137,9 @@ class ToHtml(Tokenizer):
117
137
 
118
138
  def doConvert(self) -> None:
119
139
  """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>"
140
+ self._result = ""
151
141
 
152
- if self._isNovel and self._genMode != self.M_PREVIEW:
142
+ if self._isNovel:
153
143
  # For story files, we bump the titles one level up
154
144
  h1Cl = " class='title'"
155
145
  h1 = "h1"
@@ -163,12 +153,7 @@ class ToHtml(Tokenizer):
163
153
  h3 = "h3"
164
154
  h4 = "h4"
165
155
 
166
- self._result = ""
167
-
168
- para = []
169
- pStyle = None
170
156
  lines = []
171
-
172
157
  tHandle = self._handle
173
158
 
174
159
  for tType, nHead, tText, tFormat, tStyle in self._tokens:
@@ -181,11 +166,11 @@ class ToHtml(Tokenizer):
181
166
  for c in tText:
182
167
  if c == "<":
183
168
  cText.append("&lt;")
184
- tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
169
+ tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
185
170
  i += 4
186
171
  elif c == ">":
187
172
  cText.append("&gt;")
188
- tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
173
+ tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
189
174
  i += 4
190
175
  else:
191
176
  cText.append(c)
@@ -209,7 +194,6 @@ class ToHtml(Tokenizer):
209
194
 
210
195
  if tStyle & self.A_PBB:
211
196
  aStyle.append("page-break-before: always;")
212
-
213
197
  if tStyle & self.A_PBA:
214
198
  aStyle.append("page-break-after: always;")
215
199
 
@@ -219,11 +203,13 @@ class ToHtml(Tokenizer):
219
203
  aStyle.append("margin-top: 0;")
220
204
 
221
205
  if tStyle & self.A_IND_L:
222
- aStyle.append(f"margin-left: {CONFIG.tabWidth:d}px;")
206
+ aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
223
207
  if tStyle & self.A_IND_R:
224
- aStyle.append(f"margin-right: {CONFIG.tabWidth:d}px;")
208
+ aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
209
+ if tStyle & self.A_IND_T:
210
+ aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
225
211
 
226
- if len(aStyle) > 0:
212
+ if aStyle:
227
213
  stVals = " ".join(aStyle)
228
214
  hStyle = f" style='{stVals}'"
229
215
  else:
@@ -235,37 +221,27 @@ class ToHtml(Tokenizer):
235
221
  aNm = ""
236
222
 
237
223
  # 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
224
+ if tType == self.T_TEXT:
225
+ lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
250
226
 
251
227
  elif tType == self.T_TITLE:
252
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
228
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
253
229
  lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
254
230
 
255
231
  elif tType == self.T_HEAD1:
256
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
232
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
257
233
  lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
258
234
 
259
235
  elif tType == self.T_HEAD2:
260
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
236
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
261
237
  lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
262
238
 
263
239
  elif tType == self.T_HEAD3:
264
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
240
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
265
241
  lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
266
242
 
267
243
  elif tType == self.T_HEAD4:
268
- tHead = tText.replace(nwHeadFmt.BR, "<br/>")
244
+ tHead = tText.replace(nwHeadFmt.BR, "<br>")
269
245
  lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
270
246
 
271
247
  elif tType == self.T_SEP:
@@ -274,22 +250,14 @@ class ToHtml(Tokenizer):
274
250
  elif tType == self.T_SKIP:
275
251
  lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")
276
252
 
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
253
  elif tType == self.T_SYNOPSIS and self._doSynopsis:
286
- lines.append(self._formatSynopsis(tText, True))
254
+ lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
287
255
 
288
256
  elif tType == self.T_SHORT and self._doSynopsis:
289
- lines.append(self._formatSynopsis(tText, False))
257
+ lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
290
258
 
291
259
  elif tType == self.T_COMMENT and self._doComments:
292
- lines.append(self._formatComments(tText))
260
+ lines.append(self._formatComments(self._formatText(tText, tFormat)))
293
261
 
294
262
  elif tType == self.T_KEYWORD and self._doKeywords:
295
263
  tag, text = self._formatKeywords(tText)
@@ -302,6 +270,26 @@ class ToHtml(Tokenizer):
302
270
 
303
271
  return
304
272
 
273
+ def appendFootnotes(self) -> None:
274
+ """Append the footnotes in the buffer."""
275
+ if self._usedNotes:
276
+ footnotes = self._localLookup("Footnotes")
277
+
278
+ lines = []
279
+ lines.append(f"<h3>{footnotes}</h3>\n")
280
+ lines.append("<ol>\n")
281
+ for key, index in self._usedNotes.items():
282
+ if content := self._footnotes.get(key):
283
+ text = self._formatText(*content)
284
+ lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
285
+ lines.append("</ol>\n")
286
+
287
+ result = "".join(lines)
288
+ self._result += result
289
+ self._fullHTML.append(result)
290
+
291
+ return
292
+
305
293
  def saveHtml5(self, path: str | Path) -> None:
306
294
  """Save the data to an HTML file."""
307
295
  with open(path, mode="w", encoding="utf-8") as fObj:
@@ -367,8 +355,16 @@ class ToHtml(Tokenizer):
367
355
  mScale = self._lineHeight/1.15
368
356
 
369
357
  styles = []
370
- styles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
371
- self._textFont, self._textSize
358
+ font = self._textFont
359
+ styles.append((
360
+ "body {{"
361
+ "font-family: '{0:s}'; font-size: {1:d}pt; "
362
+ "font-weight: {2:d}; font-style: {3:s};"
363
+ "}}"
364
+ ).format(
365
+ font.family(), font.pointSize(),
366
+ FONT_WEIGHTS.get(font.weight(), 400),
367
+ FONT_STYLE.get(font.style(), "normal"),
372
368
  ))
373
369
  styles.append((
374
370
  "p {{"
@@ -442,10 +438,12 @@ class ToHtml(Tokenizer):
442
438
 
443
439
  styles.append("a {color: rgb(66, 113, 174);}")
444
440
  styles.append("mark {background: rgb(255, 255, 166);}")
445
- styles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
441
+ styles.append(".keyword {color: rgb(245, 135, 31); font-weight: bold;}")
446
442
  styles.append(".break {text-align: left;}")
447
443
  styles.append(".synopsis {font-style: italic;}")
448
444
  styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
445
+ styles.append(".dialog {color: rgb(66, 113, 174);}")
446
+ styles.append(".altdialog {color: rgb(129, 55, 9);}")
449
447
 
450
448
  return styles
451
449
 
@@ -453,24 +451,63 @@ class ToHtml(Tokenizer):
453
451
  # Internal Functions
454
452
  ##
455
453
 
454
+ def _formatText(self, text: str, tFmt: T_Formats) -> str:
455
+ """Apply formatting tags to text."""
456
+ temp = text
457
+
458
+ # Build a list of all html tags that need to be inserted in the text.
459
+ # This is done in the forward direction, and a tag is only opened if it
460
+ # isn't already open, and only closed if it has previously been opened.
461
+ tags: list[tuple[int, str]] = []
462
+ state = dict.fromkeys(HTML_OPENER, False)
463
+ for pos, fmt, data in tFmt:
464
+ if m := HTML_OPENER.get(fmt):
465
+ if not state.get(fmt, True):
466
+ tags.append((pos, m[1]))
467
+ state[fmt] = True
468
+ elif m := HTML_CLOSER.get(fmt):
469
+ if state.get(m[0], False):
470
+ tags.append((pos, m[1]))
471
+ state[m[0]] = False
472
+ elif fmt == self.FMT_FNOTE:
473
+ if data in self._footnotes:
474
+ index = len(self._usedNotes) + 1
475
+ self._usedNotes[data] = index
476
+ tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
477
+ else:
478
+ tags.append((pos, "<sup>ERR</sup>"))
479
+
480
+ # Check all format types and close any tag that is still open. This
481
+ # ensures that unclosed tags don't spill over to the next paragraph.
482
+ end = len(text)
483
+ for opener, active in state.items():
484
+ if active:
485
+ closer = HTML_OPENER.get(opener, HTML_NONE)[0]
486
+ tags.append((end, HTML_CLOSER.get(closer, HTML_NONE)[1]))
487
+
488
+ # Insert all tags at their correct position, starting from the back.
489
+ # The reverse order ensures that the positions are not shifted while we
490
+ # insert tags.
491
+ for pos, tag in reversed(tags):
492
+ temp = f"{temp[:pos]}{tag}{temp[pos:]}"
493
+
494
+ # Replace all line breaks with proper HTML break tags
495
+ temp = temp.replace("\n", "<br>")
496
+
497
+ return stripEscape(temp)
498
+
456
499
  def _formatSynopsis(self, text: str, synopsis: bool) -> str:
457
500
  """Apply HTML formatting to synopsis."""
458
501
  if synopsis:
459
502
  sSynop = self._localLookup("Synopsis")
460
503
  else:
461
504
  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"
505
+ return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
466
506
 
467
507
  def _formatComments(self, text: str) -> str:
468
508
  """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"
509
+ sComm = self._localLookup("Comment")
510
+ return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
474
511
 
475
512
  def _formatKeywords(self, text: str) -> tuple[str, str]:
476
513
  """Apply HTML formatting to keywords."""
@@ -478,19 +515,16 @@ class ToHtml(Tokenizer):
478
515
  if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
479
516
  return "", ""
480
517
 
481
- result = f"<span class='tags'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
518
+ result = f"<span class='keyword'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
482
519
  if len(bits) > 1:
483
520
  if bits[0] == nwKeyWords.TAG_KEY:
484
521
  one, two = self._project.index.parseValue(bits[1])
485
- result += f"<a name='tag_{one}'>{one}</a>"
522
+ result += f"<a class='tag' name='tag_{one}'>{one}</a>"
486
523
  if two:
487
524
  result += f" | <span class='optional'>{two}</a>"
488
525
  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:])
526
+ result += ", ".join(
527
+ f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
528
+ )
493
529
 
494
530
  return bits[0][1:], result
495
-
496
- # END Class ToHtml