novelWriter 2.7.4__py3-none-any.whl → 2.8b1__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 (196) hide show
  1. novelwriter/__init__.py +8 -7
  2. novelwriter/assets/icons/font_awesome.icons +22 -4
  3. novelwriter/assets/icons/material_filled_normal.icons +20 -2
  4. novelwriter/assets/icons/material_filled_thin.icons +20 -2
  5. novelwriter/assets/icons/material_rounded_normal.icons +20 -2
  6. novelwriter/assets/icons/material_rounded_thin.icons +20 -2
  7. novelwriter/assets/icons/material_sharp_normal.icons +20 -2
  8. novelwriter/assets/icons/material_sharp_thin.icons +20 -2
  9. novelwriter/assets/icons/remix_filled.icons +20 -2
  10. novelwriter/assets/icons/remix_outline.icons +20 -2
  11. novelwriter/assets/images/welcome.webp +0 -0
  12. novelwriter/assets/manual.pdf +0 -0
  13. novelwriter/assets/manual_fr.pdf +0 -0
  14. novelwriter/assets/sample.zip +0 -0
  15. novelwriter/assets/text/credits_en.htm +61 -11
  16. novelwriter/assets/themes/aura.conf +97 -0
  17. novelwriter/assets/themes/aura_bright.conf +95 -0
  18. novelwriter/assets/themes/aura_soft.conf +97 -0
  19. novelwriter/assets/themes/b2t_garden_dark.conf +97 -0
  20. novelwriter/assets/themes/b2t_garden_light.conf +97 -0
  21. novelwriter/assets/themes/b2t_suburb_dark.conf +97 -0
  22. novelwriter/assets/themes/b2t_suburb_light.conf +97 -0
  23. novelwriter/assets/themes/b4t_classic_o_dark.conf +97 -0
  24. novelwriter/assets/themes/b4t_classic_o_light.conf +97 -0
  25. novelwriter/assets/themes/b4t_modern_c_dark.conf +97 -0
  26. novelwriter/assets/themes/b4t_modern_c_light.conf +97 -0
  27. novelwriter/assets/themes/blue_streak_dark.conf +97 -0
  28. novelwriter/assets/themes/blue_streak_light.conf +97 -0
  29. novelwriter/assets/themes/castle_day.conf +95 -0
  30. novelwriter/assets/themes/castle_night.conf +95 -0
  31. novelwriter/assets/themes/catppuccin_latte.conf +97 -0
  32. novelwriter/assets/themes/catppuccin_mocha.conf +97 -0
  33. novelwriter/assets/themes/chalky_soil.conf +95 -0
  34. novelwriter/assets/themes/chernozem.conf +95 -0
  35. novelwriter/assets/themes/cyberpunk_night.conf +88 -40
  36. novelwriter/assets/themes/default_dark.conf +89 -41
  37. novelwriter/assets/themes/default_light.conf +89 -41
  38. novelwriter/assets/themes/dracula.conf +91 -42
  39. novelwriter/assets/themes/espresso.conf +97 -0
  40. novelwriter/assets/themes/everforest_dark.conf +97 -0
  41. novelwriter/assets/themes/everforest_light.conf +97 -0
  42. novelwriter/assets/themes/floral_daydream.conf +95 -0
  43. novelwriter/assets/themes/floral_midnight.conf +95 -0
  44. novelwriter/assets/themes/full_moon.conf +95 -0
  45. novelwriter/assets/themes/grey_dark.conf +97 -0
  46. novelwriter/assets/themes/grey_light.conf +97 -0
  47. novelwriter/assets/themes/horizon_dark.conf +97 -0
  48. novelwriter/assets/themes/horizon_light.conf +97 -0
  49. novelwriter/assets/themes/jewel_case_dark.conf +95 -0
  50. novelwriter/assets/themes/jewel_case_light.conf +95 -0
  51. novelwriter/assets/themes/lcars.conf +97 -0
  52. novelwriter/assets/themes/light_owl.conf +117 -0
  53. novelwriter/assets/themes/new_moon.conf +97 -0
  54. novelwriter/assets/themes/night_owl.conf +117 -0
  55. novelwriter/assets/themes/noctis.conf +129 -0
  56. novelwriter/assets/themes/noctis_lux.conf +129 -0
  57. novelwriter/assets/themes/nord.conf +97 -0
  58. novelwriter/assets/themes/nordlicht.conf +95 -0
  59. novelwriter/assets/themes/otium_dark.conf +95 -0
  60. novelwriter/assets/themes/otium_light.conf +95 -0
  61. novelwriter/assets/themes/paragon.conf +96 -0
  62. novelwriter/assets/themes/primer_light.conf +97 -0
  63. novelwriter/assets/themes/primer_night.conf +97 -0
  64. novelwriter/assets/themes/rose_pine.conf +97 -0
  65. novelwriter/assets/themes/rose_pine_dawn.conf +97 -0
  66. novelwriter/assets/themes/ruby_day.conf +95 -0
  67. novelwriter/assets/themes/ruby_night.conf +95 -0
  68. novelwriter/assets/themes/selenium_dark.conf +95 -0
  69. novelwriter/assets/themes/selenium_light.conf +95 -0
  70. novelwriter/assets/themes/sepia_dark.conf +95 -0
  71. novelwriter/assets/themes/sepia_light.conf +95 -0
  72. novelwriter/assets/themes/snazzy.conf +102 -40
  73. novelwriter/assets/themes/solarized_dark.conf +108 -40
  74. novelwriter/assets/themes/solarized_light.conf +108 -40
  75. novelwriter/assets/themes/sultana_light.conf +95 -0
  76. novelwriter/assets/themes/sultana_night.conf +95 -0
  77. novelwriter/assets/themes/tango_dark.conf +111 -0
  78. novelwriter/assets/themes/tango_light.conf +111 -0
  79. novelwriter/assets/themes/tomorrow.conf +117 -0
  80. novelwriter/assets/themes/tomorrow_night.conf +117 -0
  81. novelwriter/assets/themes/tomorrow_night_blue.conf +117 -0
  82. novelwriter/assets/themes/tomorrow_night_bright.conf +117 -0
  83. novelwriter/assets/themes/tomorrow_night_eighties.conf +117 -0
  84. novelwriter/assets/themes/vivid_black_green.conf +97 -0
  85. novelwriter/assets/themes/vivid_black_red.conf +97 -0
  86. novelwriter/assets/themes/vivid_white_green.conf +97 -0
  87. novelwriter/assets/themes/vivid_white_red.conf +97 -0
  88. novelwriter/assets/themes/warpgate.conf +96 -0
  89. novelwriter/assets/themes/waterlily_dark.conf +95 -0
  90. novelwriter/assets/themes/waterlily_light.conf +95 -0
  91. novelwriter/common.py +47 -17
  92. novelwriter/config.py +57 -62
  93. novelwriter/constants.py +32 -6
  94. novelwriter/core/buildsettings.py +3 -23
  95. novelwriter/core/coretools.py +21 -25
  96. novelwriter/core/docbuild.py +4 -9
  97. novelwriter/core/document.py +2 -6
  98. novelwriter/core/index.py +33 -53
  99. novelwriter/core/indexdata.py +17 -22
  100. novelwriter/core/item.py +11 -35
  101. novelwriter/core/itemmodel.py +5 -21
  102. novelwriter/core/novelmodel.py +3 -7
  103. novelwriter/core/options.py +3 -4
  104. novelwriter/core/project.py +31 -21
  105. novelwriter/core/projectdata.py +2 -21
  106. novelwriter/core/projectxml.py +13 -21
  107. novelwriter/core/sessions.py +2 -4
  108. novelwriter/core/spellcheck.py +12 -13
  109. novelwriter/core/status.py +27 -20
  110. novelwriter/core/storage.py +5 -10
  111. novelwriter/core/tree.py +6 -15
  112. novelwriter/dialogs/about.py +9 -10
  113. novelwriter/dialogs/docmerge.py +17 -14
  114. novelwriter/dialogs/docsplit.py +18 -14
  115. novelwriter/dialogs/editlabel.py +15 -9
  116. novelwriter/dialogs/preferences.py +69 -68
  117. novelwriter/dialogs/projectsettings.py +88 -67
  118. novelwriter/dialogs/quotes.py +15 -10
  119. novelwriter/dialogs/wordlist.py +18 -21
  120. novelwriter/enum.py +75 -30
  121. novelwriter/error.py +6 -11
  122. novelwriter/extensions/configlayout.py +8 -34
  123. novelwriter/extensions/eventfilters.py +3 -3
  124. novelwriter/extensions/modified.py +87 -32
  125. novelwriter/extensions/novelselector.py +13 -12
  126. novelwriter/extensions/pagedsidebar.py +10 -18
  127. novelwriter/extensions/progressbars.py +5 -11
  128. novelwriter/extensions/statusled.py +3 -6
  129. novelwriter/extensions/switch.py +8 -11
  130. novelwriter/extensions/switchbox.py +2 -11
  131. novelwriter/extensions/versioninfo.py +6 -7
  132. novelwriter/formats/shared.py +10 -2
  133. novelwriter/formats/todocx.py +15 -37
  134. novelwriter/formats/tohtml.py +52 -61
  135. novelwriter/formats/tokenizer.py +33 -64
  136. novelwriter/formats/tomarkdown.py +4 -11
  137. novelwriter/formats/toodt.py +12 -71
  138. novelwriter/formats/toqdoc.py +11 -21
  139. novelwriter/formats/toraw.py +2 -6
  140. novelwriter/gui/doceditor.py +207 -245
  141. novelwriter/gui/dochighlight.py +142 -101
  142. novelwriter/gui/docviewer.py +53 -84
  143. novelwriter/gui/docviewerpanel.py +18 -41
  144. novelwriter/gui/editordocument.py +12 -17
  145. novelwriter/gui/itemdetails.py +5 -14
  146. novelwriter/gui/mainmenu.py +24 -32
  147. novelwriter/gui/noveltree.py +13 -51
  148. novelwriter/gui/outline.py +20 -61
  149. novelwriter/gui/projtree.py +40 -96
  150. novelwriter/gui/search.py +9 -24
  151. novelwriter/gui/sidebar.py +54 -22
  152. novelwriter/gui/statusbar.py +7 -22
  153. novelwriter/gui/theme.py +482 -368
  154. novelwriter/guimain.py +87 -101
  155. novelwriter/shared.py +79 -48
  156. novelwriter/splash.py +9 -5
  157. novelwriter/text/comments.py +1 -1
  158. novelwriter/text/counting.py +9 -5
  159. novelwriter/text/patterns.py +20 -15
  160. novelwriter/tools/dictionaries.py +18 -16
  161. novelwriter/tools/lipsum.py +15 -17
  162. novelwriter/tools/manusbuild.py +25 -45
  163. novelwriter/tools/manuscript.py +94 -95
  164. novelwriter/tools/manussettings.py +149 -104
  165. novelwriter/tools/noveldetails.py +10 -24
  166. novelwriter/tools/welcome.py +24 -72
  167. novelwriter/tools/writingstats.py +17 -26
  168. novelwriter/types.py +25 -13
  169. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/METADATA +7 -7
  170. novelwriter-2.8b1.dist-info/RECORD +212 -0
  171. novelwriter/assets/images/welcome-dark.jpg +0 -0
  172. novelwriter/assets/images/welcome-light.jpg +0 -0
  173. novelwriter/assets/syntax/cyberpunk_night.conf +0 -28
  174. novelwriter/assets/syntax/default_dark.conf +0 -42
  175. novelwriter/assets/syntax/default_light.conf +0 -42
  176. novelwriter/assets/syntax/dracula.conf +0 -44
  177. novelwriter/assets/syntax/grey_dark.conf +0 -29
  178. novelwriter/assets/syntax/grey_light.conf +0 -29
  179. novelwriter/assets/syntax/light_owl.conf +0 -49
  180. novelwriter/assets/syntax/night_owl.conf +0 -49
  181. novelwriter/assets/syntax/snazzy.conf +0 -42
  182. novelwriter/assets/syntax/solarized_dark.conf +0 -29
  183. novelwriter/assets/syntax/solarized_light.conf +0 -29
  184. novelwriter/assets/syntax/tango.conf +0 -39
  185. novelwriter/assets/syntax/tomorrow.conf +0 -49
  186. novelwriter/assets/syntax/tomorrow_night.conf +0 -49
  187. novelwriter/assets/syntax/tomorrow_night_blue.conf +0 -49
  188. novelwriter/assets/syntax/tomorrow_night_bright.conf +0 -49
  189. novelwriter/assets/syntax/tomorrow_night_eighties.conf +0 -49
  190. novelwriter/assets/themes/default.conf +0 -3
  191. novelwriter-2.7.4.dist-info/RECORD +0 -163
  192. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/WHEEL +0 -0
  193. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/entry_points.txt +0 -0
  194. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/LICENSE.md +0 -0
  195. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/setup/LICENSE-Apache-2.0.txt +0 -0
  196. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/top_level.txt +0 -0
@@ -21,7 +21,7 @@ General Public License for more details.
21
21
 
22
22
  You should have received a copy of the GNU General Public License
23
23
  along with this program. If not, see <https://www.gnu.org/licenses/>.
24
- """
24
+ """ # noqa
25
25
  from __future__ import annotations
26
26
 
27
27
  import logging
@@ -36,11 +36,12 @@ from PyQt6.QtGui import (
36
36
  )
37
37
 
38
38
  from novelwriter import CONFIG, SHARED
39
- from novelwriter.common import checkInt
39
+ from novelwriter.common import checkInt, utf16CharMap
40
40
  from novelwriter.constants import nwStyles, nwUnicode
41
41
  from novelwriter.enum import nwComment
42
42
  from novelwriter.text.comments import processComment
43
43
  from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
44
+ from novelwriter.types import QtTextUserProperty
44
45
 
45
46
  logger = logging.getLogger(__name__)
46
47
 
@@ -56,6 +57,7 @@ BLOCK_TITLE = 4
56
57
 
57
58
 
58
59
  class GuiDocHighlighter(QSyntaxHighlighter):
60
+ """GUI: Editor Syntax Highlighter."""
59
61
 
60
62
  __slots__ = (
61
63
  "_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel",
@@ -84,8 +86,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
84
86
 
85
87
  logger.debug("Ready: GuiDocHighlighter")
86
88
 
87
- return
88
-
89
89
  def initHighlighter(self) -> None:
90
90
  """Initialise the syntax highlighter, setting all the colour
91
91
  rules and building the RegExes.
@@ -94,8 +94,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
94
94
  syntax = SHARED.theme.syntaxTheme
95
95
 
96
96
  colEmph = syntax.emph if CONFIG.highlightEmph else None
97
- colBreak = QColor(syntax.emph)
98
- colBreak.setAlpha(64)
99
97
 
100
98
  # Create Character Formats
101
99
  self._addCharFormat("text", syntax.text)
@@ -110,8 +108,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
110
108
  self._addCharFormat("bold", colEmph, "b")
111
109
  self._addCharFormat("italic", colEmph, "i")
112
110
  self._addCharFormat("strike", syntax.hidden, "s")
111
+ self._addCharFormat("mark", syntax.mark, "bg")
113
112
  self._addCharFormat("mspaces", syntax.error, "err")
114
- self._addCharFormat("nobreak", colBreak, "bg")
113
+ self._addCharFormat("nobreak", syntax.space, "bg")
115
114
  self._addCharFormat("altdialog", syntax.dialA)
116
115
  self._addCharFormat("dialog", syntax.dialN)
117
116
  self._addCharFormat("replace", syntax.repTag)
@@ -139,7 +138,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
139
138
 
140
139
  # Multiple or Trailing Spaces
141
140
  if CONFIG.showMultiSpaces:
142
- rxRule = re.compile(r"[ ]{2,}|[ ]*$", re.UNICODE)
141
+ rxRule = re.compile(r"\s{2,}")
143
142
  hlRule = {
144
143
  0: self._hStyles["mspaces"],
145
144
  }
@@ -148,7 +147,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
148
147
  self._cmnRules.append((rxRule, hlRule))
149
148
 
150
149
  # Non-Breaking Spaces
151
- rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+", re.UNICODE)
150
+ rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+")
152
151
  hlRule = {
153
152
  0: self._hStyles["nobreak"],
154
153
  }
@@ -196,6 +195,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
196
195
  self._txtRules.append((rxRule, hlRule))
197
196
  self._cmnRules.append((rxRule, hlRule))
198
197
 
198
+ # Markdown Highlight
199
+ rxRule = REGEX_PATTERNS.markdownMark
200
+ hlRule = {
201
+ 1: self._hStyles["markup"],
202
+ 2: self._hStyles["mark"],
203
+ 3: self._hStyles["markup"],
204
+ }
205
+ self._minRules.append((rxRule, hlRule))
206
+ self._txtRules.append((rxRule, hlRule))
207
+ self._cmnRules.append((rxRule, hlRule))
208
+
199
209
  # Shortcodes
200
210
  rxRule = REGEX_PATTERNS.shortcodePlain
201
211
  hlRule = {
@@ -226,7 +236,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
226
236
  self._cmnRules.append((rxRule, hlRule))
227
237
 
228
238
  # Alignment Tags
229
- rxRule = re.compile(r"(^>{1,2}|<{1,2}$)", re.UNICODE)
239
+ rxRule = re.compile(r"(^>{1,2}|<{1,2}$)")
230
240
  hlRule = {
231
241
  1: self._hStyles["markup"],
232
242
  }
@@ -234,7 +244,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
234
244
  self._txtRules.append((rxRule, hlRule))
235
245
 
236
246
  # Auto-Replace Tags
237
- rxRule = re.compile(r"<(\S+?)>", re.UNICODE)
247
+ rxRule = re.compile(r"<(\S+?)>")
238
248
  hlRule = {
239
249
  0: self._hStyles["replace"],
240
250
  }
@@ -242,8 +252,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
242
252
  self._txtRules.append((rxRule, hlRule))
243
253
  self._cmnRules.append((rxRule, hlRule))
244
254
 
245
- return
246
-
247
255
  ##
248
256
  # Setters
249
257
  ##
@@ -251,7 +259,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
251
259
  def setSpellCheck(self, state: bool) -> None:
252
260
  """Enable/disable the real time spell checker."""
253
261
  self._spellCheck = state
254
- return
255
262
 
256
263
  def setHandle(self, tHandle: str) -> None:
257
264
  """Set the handle of the currently highlighted document."""
@@ -262,7 +269,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
262
269
  self._isNovel = item.isDocumentLayout()
263
270
  self._isInactive = item.isInactiveClass()
264
271
  logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
265
- return
266
272
 
267
273
  ##
268
274
  # Methods
@@ -280,7 +286,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
280
286
  if block.userState() & cType > 0:
281
287
  self.rehighlightBlock(block)
282
288
  logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart)))
283
- return
284
289
 
285
290
  ##
286
291
  # Highlight Block
@@ -296,27 +301,37 @@ class GuiDocHighlighter(QSyntaxHighlighter):
296
301
  if self._tHandle is None or not text:
297
302
  return
298
303
 
299
- xOff = 0
300
- hRules = None
304
+ blockLen = self.currentBlock().length()
305
+ utf16Map = None
306
+ if blockLen > len(text) + 1:
307
+ # If the lengths are different, the line contains 4 byte
308
+ # Unicode characters, and we must use a map between Python
309
+ # string indices and the UTF-16 indices used by Qt, where a
310
+ # 4 byte character occupies two slots. See #2449.
311
+ utf16Map = utf16CharMap(text)
312
+
313
+ offset = 0
314
+ rules = None
301
315
  if text.startswith("@"): # Keywords and commands
302
316
  self.setCurrentBlockState(BLOCK_META)
303
317
  index = SHARED.project.index
304
- isValid, bits, pos = index.scanThis(text)
318
+ isValid, bits, loc = index.scanThis(text)
305
319
  isGood = index.checkThese(bits, self._tHandle)
306
320
  if isValid:
307
321
  for n, bit in enumerate(bits):
308
- xPos = pos[n]
309
- xLen = len(bit)
322
+ pos = utf16Map[loc[n]] if utf16Map else loc[n]
323
+ length = utf16Map[loc[n] + len(bit)] - pos if utf16Map else len(bit)
310
324
  if n == 0 and isGood[n]:
311
- self.setFormat(xPos, xLen, self._hStyles["keyword"])
325
+ self.setFormat(pos, length, self._hStyles["keyword"])
312
326
  elif isGood[n] and not self._isInactive:
313
- one, two = index.parseValue(bit)
314
- self.setFormat(xPos, len(one), self._hStyles["tag"])
315
- if two:
316
- yPos = xPos + len(bit) - len(two)
317
- self.setFormat(yPos, len(two), self._hStyles["optional"])
327
+ a, b = index.parseValue(bit)
328
+ aLen = utf16Map[loc[n] + len(a)] - pos if utf16Map else len(a)
329
+ self.setFormat(pos, aLen, self._hStyles["tag"])
330
+ if b:
331
+ bLen = utf16Map[loc[n] + len(b)] - pos if utf16Map else len(b)
332
+ self.setFormat(pos + length - bLen, bLen, self._hStyles["optional"])
318
333
  elif not self._isInactive:
319
- self.setFormat(xPos, xLen, self._hStyles["invalid"])
334
+ self.setFormat(pos, length, self._hStyles["invalid"])
320
335
 
321
336
  # We never want to run the spell checker on keyword/values,
322
337
  # so we force a return here
@@ -327,98 +342,118 @@ class GuiDocHighlighter(QSyntaxHighlighter):
327
342
 
328
343
  if text.startswith("# "): # Heading 1
329
344
  self.setFormat(0, 1, self._hStyles["head1h"])
330
- self.setFormat(1, len(text), self._hStyles["header1"])
345
+ self.setFormat(1, blockLen, self._hStyles["header1"])
331
346
 
332
347
  elif text.startswith("## "): # Heading 2
333
348
  self.setFormat(0, 2, self._hStyles["head2h"])
334
- self.setFormat(2, len(text), self._hStyles["header2"])
349
+ self.setFormat(2, blockLen, self._hStyles["header2"])
335
350
 
336
351
  elif text.startswith("### "): # Heading 3
337
352
  self.setFormat(0, 3, self._hStyles["head3h"])
338
- self.setFormat(3, len(text), self._hStyles["header3"])
353
+ self.setFormat(3, blockLen, self._hStyles["header3"])
339
354
 
340
355
  elif text.startswith("#### "): # Heading 4
341
356
  self.setFormat(0, 4, self._hStyles["head4h"])
342
- self.setFormat(4, len(text), self._hStyles["header4"])
357
+ self.setFormat(4, blockLen, self._hStyles["header4"])
343
358
 
344
359
  elif text.startswith("#! "): # Title
345
360
  self.setFormat(0, 2, self._hStyles["head1h"])
346
- self.setFormat(2, len(text), self._hStyles["header1"])
361
+ self.setFormat(2, blockLen, self._hStyles["header1"])
347
362
 
348
363
  elif text.startswith("##! "): # Unnumbered
349
364
  self.setFormat(0, 3, self._hStyles["head2h"])
350
- self.setFormat(3, len(text), self._hStyles["header2"])
365
+ self.setFormat(3, blockLen, self._hStyles["header2"])
351
366
 
352
367
  elif text.startswith("###! "): # Alternative Scene
353
368
  self.setFormat(0, 4, self._hStyles["head3h"])
354
- self.setFormat(4, len(text), self._hStyles["header3"])
369
+ self.setFormat(4, blockLen, self._hStyles["header3"])
355
370
 
356
371
  elif text.startswith("%"): # Comments
357
372
  self.setCurrentBlockState(BLOCK_TEXT)
358
- hRules = self._cmnRules
359
-
360
- cStyle, cMod, _, cDot, cPos = processComment(text)
361
- cLen = len(text) - cPos
362
- xOff = cPos
363
- if cStyle == nwComment.PLAIN:
364
- self.setFormat(0, cLen, self._hStyles["hidden"])
365
- elif cStyle == nwComment.IGNORE:
366
- self.setFormat(0, cLen, self._hStyles["strike"])
373
+ rules = self._cmnRules
374
+
375
+ style, mod, _, dot, pos = processComment(text)
376
+ offset = pos
377
+ if utf16Map:
378
+ dot = utf16Map[dot]
379
+ pos = utf16Map[pos]
380
+ length = blockLen - pos
381
+ if style == nwComment.PLAIN:
382
+ self.setFormat(0, length, self._hStyles["hidden"])
383
+ elif style == nwComment.IGNORE:
384
+ self.setFormat(0, length, self._hStyles["strike"])
367
385
  return # No more processing for these
368
- elif cMod:
369
- self.setFormat(0, cDot, self._hStyles["modifier"])
370
- self.setFormat(cDot, cPos - cDot, self._hStyles["value"])
371
- self.setFormat(cPos, cLen, self._hStyles["note"])
386
+ elif mod:
387
+ self.setFormat(0, dot, self._hStyles["modifier"])
388
+ self.setFormat(dot, pos - dot, self._hStyles["value"])
389
+ self.setFormat(pos, length, self._hStyles["note"])
372
390
  else:
373
- self.setFormat(0, cPos, self._hStyles["modifier"])
374
- self.setFormat(cPos, cLen, self._hStyles["note"])
391
+ self.setFormat(0, pos, self._hStyles["modifier"])
392
+ self.setFormat(pos, length, self._hStyles["note"])
375
393
 
376
394
  elif text.startswith("["): # Special Command
377
395
  self.setCurrentBlockState(BLOCK_TEXT)
378
- hRules = self._txtRules if self._isNovel else self._minRules
396
+ rules = self._txtRules if self._isNovel else self._minRules
379
397
 
380
- sText = text.rstrip().lower()
381
- if sText in ("[newpage]", "[new page]", "[vspace]"):
382
- self.setFormat(0, len(text), self._hStyles["code"])
398
+ check = text.rstrip().lower()
399
+ if check in ("[newpage]", "[new page]", "[vspace]"):
400
+ self.setFormat(0, blockLen, self._hStyles["code"])
383
401
  return
384
- elif sText.startswith("[vspace:") and sText.endswith("]"):
385
- tLen = len(sText)
386
- tVal = checkInt(sText[8:-1], 0)
387
- cVal = "value" if tVal > 0 else "invalid"
402
+ elif check.startswith("[vspace:") and check.endswith("]"):
403
+ value = checkInt(check[8:-1], 0)
404
+ style = "value" if value > 0 else "invalid"
388
405
  self.setFormat(0, 8, self._hStyles["code"])
389
- self.setFormat(8, tLen-9, self._hStyles[cVal])
390
- self.setFormat(tLen-1, tLen, self._hStyles["code"])
406
+ self.setFormat(8, blockLen-10, self._hStyles[style])
407
+ self.setFormat(blockLen-2, blockLen, self._hStyles["code"])
391
408
  return
392
409
 
393
410
  else: # Text Paragraph
394
411
  self.setCurrentBlockState(BLOCK_TEXT)
395
- hRules = self._txtRules if self._isNovel else self._minRules
412
+ rules = self._txtRules if self._isNovel else self._minRules
396
413
  if self._isNovel and self._dialogParser.enabled:
397
- for pos, end in self._dialogParser(text):
398
- length = end - pos
399
- self.setFormat(pos, length, self._hStyles["dialog"])
400
-
401
- if hRules:
402
- for rX, hRule in hRules:
403
- for res in re.finditer(rX, text[xOff:]):
404
- for xM, hFmt in hRule.items():
405
- xPos = res.start(xM) + xOff
406
- xEnd = res.end(xM) + xOff
407
- for x in range(xPos, xEnd):
408
- cFmt = self.format(x)
409
- if cFmt.fontStyleName() != "markup":
410
- cFmt.merge(hFmt)
411
- self.setFormat(x, 1, cFmt)
414
+ if utf16Map:
415
+ for pos, end in self._dialogParser(text):
416
+ pos = utf16Map[pos]
417
+ end = utf16Map[end]
418
+ self.setFormat(pos, end - pos, self._hStyles["dialog"])
419
+ else:
420
+ for pos, end in self._dialogParser(text):
421
+ self.setFormat(pos, end - pos, self._hStyles["dialog"])
422
+
423
+ if rules:
424
+ if utf16Map:
425
+ for rX, hRule in rules:
426
+ for res in re.finditer(rX, text[offset:]):
427
+ for x, hFmt in hRule.items():
428
+ pos = res.start(x) + offset
429
+ end = res.end(x) + offset
430
+ for x in range(pos, end):
431
+ m = utf16Map[x]
432
+ cFmt = self.format(m)
433
+ if not cFmt.property(QtTextUserProperty):
434
+ cFmt.merge(hFmt)
435
+ self.setFormat(m, utf16Map[x+1] - m, cFmt)
436
+ else:
437
+ for rX, hRule in rules:
438
+ for res in re.finditer(rX, text[offset:]):
439
+ for x, hFmt in hRule.items():
440
+ pos = res.start(x) + offset
441
+ end = res.end(x) + offset
442
+ for x in range(pos, end):
443
+ cFmt = self.format(x)
444
+ if not cFmt.property(QtTextUserProperty):
445
+ cFmt.merge(hFmt)
446
+ self.setFormat(x, 1, cFmt)
412
447
 
413
448
  data = self.currentBlockUserData()
414
449
  if not isinstance(data, TextBlockData):
415
450
  data = TextBlockData()
416
451
  self.setCurrentBlockUserData(data)
417
452
 
418
- data.processText(text, xOff)
453
+ data.processText(text, offset)
419
454
  if self._spellCheck:
420
- for xPos, xEnd in data.spellCheck():
421
- for x in range(xPos, xEnd):
455
+ for pos, end, _ in data.spellCheck(utf16Map):
456
+ for x in range(pos, end):
422
457
  cFmt = self.format(x)
423
458
  cFmt.merge(self._spellErr)
424
459
  self.setFormat(x, 1, cFmt)
@@ -435,10 +470,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
435
470
  ) -> None:
436
471
  """Generate a highlighter character format."""
437
472
  charFormat = QTextCharFormat()
438
- charFormat.setFontStyleName(name)
439
-
440
- if color:
441
- charFormat.setForeground(color)
473
+ blockMerge = name == "markup"
474
+ charFormat.setProperty(QtTextUserProperty, blockMerge)
442
475
 
443
476
  if style:
444
477
  styles = style.split(",")
@@ -455,16 +488,23 @@ class GuiDocHighlighter(QSyntaxHighlighter):
455
488
  charFormat.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline)
456
489
  if "bg" in styles and color is not None:
457
490
  charFormat.setBackground(QBrush(color, Qt.BrushStyle.SolidPattern))
491
+ color = None
492
+
493
+ if color:
494
+ charFormat.setForeground(color)
458
495
 
459
496
  if size:
460
497
  charFormat.setFontPointSize(round(size*CONFIG.textFont.pointSize()))
461
498
 
462
499
  self._hStyles[name] = charFormat
463
500
 
464
- return
465
-
466
501
 
467
502
  class TextBlockData(QTextBlockUserData):
503
+ """Custom QTextBlock Data.
504
+
505
+ Custom data stored in a single text block. The spell check state is
506
+ cached here and used when correcting misspelled text.
507
+ """
468
508
 
469
509
  __slots__ = ("_metaData", "_offset", "_spellErrors", "_text")
470
510
 
@@ -473,8 +513,7 @@ class TextBlockData(QTextBlockUserData):
473
513
  self._text = ""
474
514
  self._offset = 0
475
515
  self._metaData: list[tuple[int, int, str, str]] = []
476
- self._spellErrors: list[tuple[int, int]] = []
477
- return
516
+ self._spellErrors: list[tuple[int, int, str]] = []
478
517
 
479
518
  @property
480
519
  def metaData(self) -> list[tuple[int, int, str, str]]:
@@ -482,7 +521,7 @@ class TextBlockData(QTextBlockUserData):
482
521
  return self._metaData
483
522
 
484
523
  @property
485
- def spellErrors(self) -> list[tuple[int, int]]:
524
+ def spellErrors(self) -> list[tuple[int, int, str]]:
486
525
  """Return spell error data from last check."""
487
526
  return self._spellErrors
488
527
 
@@ -505,22 +544,24 @@ class TextBlockData(QTextBlockUserData):
505
544
  text = f"{text[:s]}{pad}{text[e:]}"
506
545
  self._metaData.append((s, e, res.group(0), "url"))
507
546
 
508
- self._text = text.replace("\u02bc", "'")
547
+ self._text = text.replace("\u02bc", "'").replace("_", " ")
509
548
  self._offset = offset
510
549
 
511
- return
512
-
513
- def spellCheck(self) -> list[tuple[int, int]]:
550
+ def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]:
514
551
  """Run the spell checker and cache the result, and return the
515
552
  list of spell check errors.
516
553
  """
517
- self._spellErrors = []
518
- checker = SHARED.spelling
519
- for res in RX_WORDS.finditer(self._text.replace("_", " "), self._offset):
520
- if (
521
- (word := res.group(0))
522
- and not (word.isnumeric() or word.isupper() or checker.checkWord(word))
523
- ):
524
- self._spellErrors.append((res.start(0), res.end(0)))
525
-
554
+ spell = SHARED.spelling
555
+ if utf16Map:
556
+ self._spellErrors = [
557
+ (utf16Map[r.start(0)], utf16Map[r.end(0)], w)
558
+ for r in RX_WORDS.finditer(self._text, self._offset)
559
+ if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w))
560
+ ]
561
+ else:
562
+ self._spellErrors = [
563
+ (r.start(0), r.end(0), w)
564
+ for r in RX_WORDS.finditer(self._text, self._offset)
565
+ if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w))
566
+ ]
526
567
  return self._spellErrors