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
@@ -28,26 +28,28 @@ import logging
28
28
 
29
29
  from time import time
30
30
 
31
- from PyQt5.QtCore import Qt, QRegularExpression
31
+ from PyQt5.QtCore import QRegularExpression, Qt
32
32
  from PyQt5.QtGui import (
33
33
  QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData,
34
34
  QTextCharFormat, QTextDocument
35
35
  )
36
36
 
37
37
  from novelwriter import CONFIG, SHARED
38
- from novelwriter.enum import nwComment
39
38
  from novelwriter.common import checkInt
40
- from novelwriter.constants import nwRegEx, nwUnicode
39
+ from novelwriter.constants import nwHeaders, nwRegEx, nwUnicode
41
40
  from novelwriter.core.index import processComment
41
+ from novelwriter.enum import nwComment
42
+ from novelwriter.text.patterns import REGEX_PATTERNS
43
+ from novelwriter.types import QRegExUnicode
42
44
 
43
45
  logger = logging.getLogger(__name__)
44
46
 
45
47
  SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
46
- SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
48
+ SPELLRX.setPatternOptions(QRegExUnicode)
47
49
  SPELLSC = QRegularExpression(nwRegEx.FMT_SC)
48
- SPELLSC.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
50
+ SPELLSC.setPatternOptions(QRegExUnicode)
49
51
  SPELLSV = QRegularExpression(nwRegEx.FMT_SV)
50
- SPELLSV.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
52
+ SPELLSV.setPatternOptions(QRegExUnicode)
51
53
 
52
54
  BLOCK_NONE = 0
53
55
  BLOCK_TEXT = 1
@@ -57,8 +59,10 @@ BLOCK_TITLE = 4
57
59
 
58
60
  class GuiDocHighlighter(QSyntaxHighlighter):
59
61
 
60
- __slots__ = ("_tHandle", "_isInactive", "_spellCheck", "_spellErr",
61
- "_hRules", "_hStyles", "_rxRules")
62
+ __slots__ = (
63
+ "_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
64
+ "_hStyles", "_minRules", "_txtRules", "_cmnRules",
65
+ )
62
66
 
63
67
  def __init__(self, document: QTextDocument) -> None:
64
68
  super().__init__(document)
@@ -66,13 +70,15 @@ class GuiDocHighlighter(QSyntaxHighlighter):
66
70
  logger.debug("Create: GuiDocHighlighter")
67
71
 
68
72
  self._tHandle = None
73
+ self._isNovel = False
69
74
  self._isInactive = False
70
75
  self._spellCheck = False
71
76
  self._spellErr = QTextCharFormat()
72
77
 
73
- self._hRules: list[tuple[str, dict]] = []
74
78
  self._hStyles: dict[str, QTextCharFormat] = {}
75
- self._rxRules: list[tuple[QRegularExpression, dict[str, QTextCharFormat]]] = []
79
+ self._minRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
80
+ self._txtRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
81
+ self._cmnRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
76
82
 
77
83
  self.initHighlighter()
78
84
 
@@ -90,145 +96,164 @@ class GuiDocHighlighter(QSyntaxHighlighter):
90
96
  colBreak = QColor(SHARED.theme.colEmph)
91
97
  colBreak.setAlpha(64)
92
98
 
93
- self._hRules = []
94
- self._hStyles = {
95
- "header1": self._makeFormat(SHARED.theme.colHead, "bold", 1.8),
96
- "header2": self._makeFormat(SHARED.theme.colHead, "bold", 1.6),
97
- "header3": self._makeFormat(SHARED.theme.colHead, "bold", 1.4),
98
- "header4": self._makeFormat(SHARED.theme.colHead, "bold", 1.2),
99
- "head1h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.8),
100
- "head2h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.6),
101
- "head3h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.4),
102
- "head4h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.2),
103
- "bold": self._makeFormat(colEmph, "bold"),
104
- "italic": self._makeFormat(colEmph, "italic"),
105
- "strike": self._makeFormat(SHARED.theme.colHidden, "strike"),
106
- "mspaces": self._makeFormat(SHARED.theme.colError, "errline"),
107
- "nobreak": self._makeFormat(colBreak, "background"),
108
- "dialogue1": self._makeFormat(SHARED.theme.colDialN),
109
- "dialogue2": self._makeFormat(SHARED.theme.colDialD),
110
- "dialogue3": self._makeFormat(SHARED.theme.colDialS),
111
- "replace": self._makeFormat(SHARED.theme.colRepTag),
112
- "hidden": self._makeFormat(SHARED.theme.colHidden),
113
- "code": self._makeFormat(SHARED.theme.colCode),
114
- "keyword": self._makeFormat(SHARED.theme.colKey),
115
- "modifier": self._makeFormat(SHARED.theme.colMod),
116
- "value": self._makeFormat(SHARED.theme.colVal),
117
- "optional": self._makeFormat(SHARED.theme.colOpt),
118
- "codevalue": self._makeFormat(SHARED.theme.colVal),
119
- "codeinval": self._makeFormat(None, "errline"),
120
- }
99
+ # Create Character Formats
100
+ self._addCharFormat("text", SHARED.theme.colText)
101
+ self._addCharFormat("header1", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[1])
102
+ self._addCharFormat("header2", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[2])
103
+ self._addCharFormat("header3", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[3])
104
+ self._addCharFormat("header4", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[4])
105
+ self._addCharFormat("head1h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[1])
106
+ self._addCharFormat("head2h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[2])
107
+ self._addCharFormat("head3h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[3])
108
+ self._addCharFormat("head4h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[4])
109
+ self._addCharFormat("bold", colEmph, "b")
110
+ self._addCharFormat("italic", colEmph, "i")
111
+ self._addCharFormat("strike", SHARED.theme.colHidden, "s")
112
+ self._addCharFormat("mspaces", SHARED.theme.colError, "err")
113
+ self._addCharFormat("nobreak", colBreak, "bg")
114
+ self._addCharFormat("altdialog", SHARED.theme.colDialA)
115
+ self._addCharFormat("dialog", SHARED.theme.colDialN)
116
+ self._addCharFormat("replace", SHARED.theme.colRepTag)
117
+ self._addCharFormat("hidden", SHARED.theme.colHidden)
118
+ self._addCharFormat("markup", SHARED.theme.colHidden)
119
+ self._addCharFormat("note", SHARED.theme.colNote)
120
+ self._addCharFormat("code", SHARED.theme.colCode)
121
+ self._addCharFormat("keyword", SHARED.theme.colKey)
122
+ self._addCharFormat("tag", SHARED.theme.colTag)
123
+ self._addCharFormat("modifier", SHARED.theme.colMod)
124
+ self._addCharFormat("value", SHARED.theme.colVal)
125
+ self._addCharFormat("optional", SHARED.theme.colOpt)
126
+ self._addCharFormat("invalid", None, "err")
121
127
 
122
128
  # Cache Spell Error Format
123
129
  self._spellErr = QTextCharFormat()
124
130
  self._spellErr.setUnderlineColor(SHARED.theme.colSpell)
125
131
  self._spellErr.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline)
126
132
 
133
+ self._txtRules.clear()
134
+ self._cmnRules.clear()
135
+
127
136
  # Multiple or Trailing Spaces
128
137
  if CONFIG.showMultiSpaces:
129
- self._hRules.append((
130
- r"[ ]{2,}|[ ]*$", {
131
- 0: self._hStyles["mspaces"],
132
- }
133
- ))
138
+ rxRule = QRegularExpression(r"[ ]{2,}|[ ]*$")
139
+ rxRule.setPatternOptions(QRegExUnicode)
140
+ hlRule = {
141
+ 0: self._hStyles["mspaces"],
142
+ }
143
+ self._minRules.append((rxRule, hlRule))
144
+ self._txtRules.append((rxRule, hlRule))
145
+ self._cmnRules.append((rxRule, hlRule))
134
146
 
135
147
  # Non-Breaking Spaces
136
- self._hRules.append((
137
- f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+", {
138
- 0: self._hStyles["nobreak"],
148
+ rxRule = QRegularExpression(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+")
149
+ rxRule.setPatternOptions(QRegExUnicode)
150
+ hlRule = {
151
+ 0: self._hStyles["nobreak"],
152
+ }
153
+ self._minRules.append((rxRule, hlRule))
154
+ self._txtRules.append((rxRule, hlRule))
155
+ self._cmnRules.append((rxRule, hlRule))
156
+
157
+ # Dialogue
158
+ if CONFIG.dialogStyle > 0:
159
+ rxRule = REGEX_PATTERNS.dialogStyle
160
+ hlRule = {
161
+ 0: self._hStyles["dialog"],
139
162
  }
140
- ))
141
-
142
- # Quoted Strings
143
- if CONFIG.highlightQuotes:
144
- fmtDblO = CONFIG.fmtDQuoteOpen
145
- fmtDblC = CONFIG.fmtDQuoteClose
146
- fmtSngO = CONFIG.fmtSQuoteOpen
147
- fmtSngC = CONFIG.fmtSQuoteClose
148
-
149
- # Straight Quotes
150
- if not (fmtDblO == fmtDblC == "\""):
151
- self._hRules.append((
152
- "(\\B\")(.*?)(\"\\B)", {
153
- 0: self._hStyles["dialogue1"],
154
- }
155
- ))
156
-
157
- # Double Quotes
158
- dblEnd = "|$" if CONFIG.allowOpenDQuote else ""
159
- self._hRules.append((
160
- f"(\\B{fmtDblO})(.*?)({fmtDblC}\\B{dblEnd})", {
161
- 0: self._hStyles["dialogue2"],
162
- }
163
- ))
164
-
165
- # Single Quotes
166
- sngEnd = "|$" if CONFIG.allowOpenSQuote else ""
167
- self._hRules.append((
168
- f"(\\B{fmtSngO})(.*?)({fmtSngC}\\B{sngEnd})", {
169
- 0: self._hStyles["dialogue3"],
170
- }
171
- ))
172
-
173
- # Markdown Syntax
174
- self._hRules.append((
175
- nwRegEx.FMT_EI, {
176
- 1: self._hStyles["hidden"],
177
- 2: self._hStyles["italic"],
178
- 3: self._hStyles["hidden"],
163
+ self._txtRules.append((rxRule, hlRule))
164
+
165
+ if CONFIG.dialogLine:
166
+ rxRule = REGEX_PATTERNS.dialogLine
167
+ hlRule = {
168
+ 0: self._hStyles["dialog"],
179
169
  }
180
- ))
181
- self._hRules.append((
182
- nwRegEx.FMT_EB, {
183
- 1: self._hStyles["hidden"],
184
- 2: self._hStyles["bold"],
185
- 3: self._hStyles["hidden"],
170
+ self._txtRules.append((rxRule, hlRule))
171
+
172
+ if CONFIG.narratorBreak:
173
+ rxRule = REGEX_PATTERNS.narratorBreak
174
+ hlRule = {
175
+ 0: self._hStyles["text"],
186
176
  }
187
- ))
188
- self._hRules.append((
189
- nwRegEx.FMT_ST, {
190
- 1: self._hStyles["hidden"],
191
- 2: self._hStyles["strike"],
192
- 3: self._hStyles["hidden"],
177
+ self._txtRules.append((rxRule, hlRule))
178
+
179
+ if CONFIG.altDialogOpen and CONFIG.altDialogClose:
180
+ rxRule = REGEX_PATTERNS.altDialogStyle
181
+ hlRule = {
182
+ 0: self._hStyles["altdialog"],
193
183
  }
194
- ))
184
+ self._txtRules.append((rxRule, hlRule))
185
+
186
+ # Markdown Italic
187
+ rxRule = REGEX_PATTERNS.markdownItalic
188
+ hlRule = {
189
+ 1: self._hStyles["markup"],
190
+ 2: self._hStyles["italic"],
191
+ 3: self._hStyles["markup"],
192
+ }
193
+ self._minRules.append((rxRule, hlRule))
194
+ self._txtRules.append((rxRule, hlRule))
195
+ self._cmnRules.append((rxRule, hlRule))
196
+
197
+ # Markdown Bold
198
+ rxRule = REGEX_PATTERNS.markdownBold
199
+ hlRule = {
200
+ 1: self._hStyles["markup"],
201
+ 2: self._hStyles["bold"],
202
+ 3: self._hStyles["markup"],
203
+ }
204
+ self._minRules.append((rxRule, hlRule))
205
+ self._txtRules.append((rxRule, hlRule))
206
+ self._cmnRules.append((rxRule, hlRule))
207
+
208
+ # Markdown Strikethrough
209
+ rxRule = REGEX_PATTERNS.markdownStrike
210
+ hlRule = {
211
+ 1: self._hStyles["markup"],
212
+ 2: self._hStyles["strike"],
213
+ 3: self._hStyles["markup"],
214
+ }
215
+ self._minRules.append((rxRule, hlRule))
216
+ self._txtRules.append((rxRule, hlRule))
217
+ self._cmnRules.append((rxRule, hlRule))
195
218
 
196
219
  # Shortcodes
197
- self._hRules.append((
198
- nwRegEx.FMT_SC, {
199
- 1: self._hStyles["code"],
200
- }
201
- ))
220
+ rxRule = REGEX_PATTERNS.shortcodePlain
221
+ hlRule = {
222
+ 1: self._hStyles["code"],
223
+ }
224
+ self._minRules.append((rxRule, hlRule))
225
+ self._txtRules.append((rxRule, hlRule))
226
+ self._cmnRules.append((rxRule, hlRule))
202
227
 
203
228
  # Shortcodes w/Value
204
- self._hRules.append((
205
- nwRegEx.FMT_SV, {
206
- 1: self._hStyles["code"],
207
- 2: self._hStyles["codevalue"],
208
- 3: self._hStyles["code"],
209
- }
210
- ))
229
+ rxRule = REGEX_PATTERNS.shortcodeValue
230
+ hlRule = {
231
+ 1: self._hStyles["code"],
232
+ 2: self._hStyles["value"],
233
+ 3: self._hStyles["code"],
234
+ }
235
+ self._minRules.append((rxRule, hlRule))
236
+ self._txtRules.append((rxRule, hlRule))
237
+ self._cmnRules.append((rxRule, hlRule))
211
238
 
212
239
  # Alignment Tags
213
- self._hRules.append((
214
- r"(^>{1,2}|<{1,2}$)", {
215
- 1: self._hStyles["hidden"],
216
- }
217
- ))
240
+ rxRule = QRegularExpression(r"(^>{1,2}|<{1,2}$)")
241
+ rxRule.setPatternOptions(QRegExUnicode)
242
+ hlRule = {
243
+ 1: self._hStyles["markup"],
244
+ }
245
+ self._minRules.append((rxRule, hlRule))
246
+ self._txtRules.append((rxRule, hlRule))
218
247
 
219
248
  # Auto-Replace Tags
220
- self._hRules.append((
221
- r"<(\S+?)>", {
222
- 0: self._hStyles["replace"],
223
- }
224
- ))
225
-
226
- # Build a QRegExp for each highlight pattern
227
- self._rxRules = []
228
- for regEx, regRules in self._hRules:
229
- hReg = QRegularExpression(regEx)
230
- hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
231
- self._rxRules.append((hReg, regRules))
249
+ rxRule = QRegularExpression(r"<(\S+?)>")
250
+ rxRule.setPatternOptions(QRegExUnicode)
251
+ hlRule = {
252
+ 0: self._hStyles["replace"],
253
+ }
254
+ self._minRules.append((rxRule, hlRule))
255
+ self._txtRules.append((rxRule, hlRule))
256
+ self._cmnRules.append((rxRule, hlRule))
232
257
 
233
258
  return
234
259
 
@@ -244,9 +269,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
244
269
  def setHandle(self, tHandle: str) -> None:
245
270
  """Set the handle of the currently highlighted document."""
246
271
  self._tHandle = tHandle
247
- self._isInactive = (
248
- item.isInactiveClass() if (item := SHARED.project.tree[tHandle]) else False
249
- )
272
+ self._isNovel = False
273
+ self._isInactive = False
274
+ if item := SHARED.project.tree[tHandle]:
275
+ self._isNovel = item.isDocumentLayout()
276
+ self._isInactive = item.isInactiveClass()
250
277
  logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
251
278
  return
252
279
 
@@ -282,6 +309,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
282
309
  if self._tHandle is None or not text:
283
310
  return
284
311
 
312
+ xOff = 0
313
+ hRules = None
285
314
  if text.startswith("@"): # Keywords and commands
286
315
  self.setCurrentBlockState(BLOCK_META)
287
316
  index = SHARED.project.index
@@ -295,12 +324,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
295
324
  self.setFormat(xPos, xLen, self._hStyles["keyword"])
296
325
  elif isGood[n] and not self._isInactive:
297
326
  one, two = index.parseValue(bit)
298
- self.setFormat(xPos, len(one), self._hStyles["value"])
327
+ self.setFormat(xPos, len(one), self._hStyles["tag"])
299
328
  if two:
300
329
  yPos = xPos + len(bit) - len(two)
301
330
  self.setFormat(yPos, len(two), self._hStyles["optional"])
302
331
  elif not self._isInactive:
303
- self.setFormat(xPos, xLen, self._hStyles["codeinval"])
332
+ self.setFormat(xPos, xLen, self._hStyles["invalid"])
304
333
 
305
334
  # We never want to run the spell checker on keyword/values,
306
335
  # so we force a return here
@@ -339,43 +368,58 @@ class GuiDocHighlighter(QSyntaxHighlighter):
339
368
 
340
369
  elif text.startswith("%"): # Comments
341
370
  self.setCurrentBlockState(BLOCK_TEXT)
342
- cStyle, _, cPos = processComment(text)
371
+ hRules = self._cmnRules
372
+
373
+ cStyle, cMod, _, cDot, cPos = processComment(text)
374
+ cLen = len(text) - cPos
375
+ xOff = cPos
343
376
  if cStyle == nwComment.PLAIN:
344
- self.setFormat(0, len(text), self._hStyles["hidden"])
377
+ self.setFormat(0, cLen, self._hStyles["hidden"])
378
+ elif cStyle == nwComment.IGNORE:
379
+ self.setFormat(0, cLen, self._hStyles["strike"])
380
+ return # No more processing for these
381
+ elif cMod:
382
+ self.setFormat(0, cDot, self._hStyles["modifier"])
383
+ self.setFormat(cDot, cPos - cDot, self._hStyles["value"])
384
+ self.setFormat(cPos, cLen, self._hStyles["note"])
345
385
  else:
346
386
  self.setFormat(0, cPos, self._hStyles["modifier"])
347
- self.setFormat(cPos, len(text), self._hStyles["hidden"])
387
+ self.setFormat(cPos, cLen, self._hStyles["note"])
348
388
 
349
- else: # Text Paragraph
389
+ elif text.startswith("["): # Special Command
390
+ self.setCurrentBlockState(BLOCK_TEXT)
391
+ hRules = self._txtRules if self._isNovel else self._minRules
392
+
393
+ sText = text.rstrip().lower()
394
+ if sText in ("[newpage]", "[new page]", "[vspace]"):
395
+ self.setFormat(0, len(text), self._hStyles["code"])
396
+ return
397
+ elif sText.startswith("[vspace:") and sText.endswith("]"):
398
+ tLen = len(sText)
399
+ tVal = checkInt(sText[8:-1], 0)
400
+ cVal = "value" if tVal > 0 else "invalid"
401
+ self.setFormat(0, 8, self._hStyles["code"])
402
+ self.setFormat(8, tLen-9, self._hStyles[cVal])
403
+ self.setFormat(tLen-1, tLen, self._hStyles["code"])
404
+ return
350
405
 
351
- if text.startswith("["): # Special Command
352
- sText = text.rstrip().lower()
353
- if sText in ("[newpage]", "[new page]", "[vspace]"):
354
- self.setFormat(0, len(text), self._hStyles["code"])
355
- return
356
- elif sText.startswith("[vspace:") and sText.endswith("]"):
357
- tLen = len(sText)
358
- tVal = checkInt(sText[8:-1], 0)
359
- cVal = "codevalue" if tVal > 0 else "codeinval"
360
- self.setFormat(0, 8, self._hStyles["code"])
361
- self.setFormat(8, tLen-9, self._hStyles[cVal])
362
- self.setFormat(tLen-1, tLen, self._hStyles["code"])
363
- return
364
-
365
- # Regular Text
406
+ else: # Text Paragraph
366
407
  self.setCurrentBlockState(BLOCK_TEXT)
367
- for rX, xFmt in self._rxRules:
368
- rxItt = rX.globalMatch(text, 0)
408
+ hRules = self._txtRules if self._isNovel else self._minRules
409
+
410
+ if hRules:
411
+ for rX, hRule in hRules:
412
+ rxItt = rX.globalMatch(text, xOff)
369
413
  while rxItt.hasNext():
370
414
  rxMatch = rxItt.next()
371
- for xM in xFmt:
415
+ for xM, hFmt in hRule.items():
372
416
  xPos = rxMatch.capturedStart(xM)
373
- xLen = rxMatch.capturedLength(xM)
374
- for x in range(xPos, xPos+xLen):
375
- spFmt = self.format(x)
376
- if spFmt != self._hStyles["hidden"]:
377
- spFmt.merge(xFmt[xM])
378
- self.setFormat(x, 1, spFmt)
417
+ xEnd = rxMatch.capturedEnd(xM)
418
+ for x in range(xPos, xEnd):
419
+ cFmt = self.format(x)
420
+ if cFmt.fontStyleName() != "markup":
421
+ cFmt.merge(hFmt)
422
+ self.setFormat(x, 1, cFmt)
379
423
 
380
424
  data = self.currentBlockUserData()
381
425
  if not isinstance(data, TextBlockData):
@@ -383,11 +427,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
383
427
  self.setCurrentBlockUserData(data)
384
428
 
385
429
  if self._spellCheck:
386
- for xPos, xLen in data.spellCheck(text):
430
+ for xPos, xLen in data.spellCheck(text, xOff):
387
431
  for x in range(xPos, xPos+xLen):
388
- spFmt = self.format(x)
389
- spFmt.merge(self._spellErr)
390
- self.setFormat(x, 1, spFmt)
432
+ cFmt = self.format(x)
433
+ cFmt.merge(self._spellErr)
434
+ self.setFormat(x, 1, cFmt)
391
435
 
392
436
  return
393
437
 
@@ -395,36 +439,37 @@ class GuiDocHighlighter(QSyntaxHighlighter):
395
439
  # Internal Functions
396
440
  ##
397
441
 
398
- def _makeFormat(self, color: QColor | None = None, style: str | None = None,
399
- size: float | None = None) -> QTextCharFormat:
400
- """Generate a valid character format to be applied to the text
401
- that is to be highlighted.
402
- """
442
+ def _addCharFormat(
443
+ self, name: str, color: QColor | None = None,
444
+ style: str | None = None, size: float | None = None
445
+ ) -> None:
446
+ """Generate a highlighter character format."""
403
447
  charFormat = QTextCharFormat()
448
+ charFormat.setFontStyleName(name)
404
449
 
405
- if color is not None:
450
+ if color:
406
451
  charFormat.setForeground(color)
407
452
 
408
- if style is not None:
453
+ if style:
409
454
  styles = style.split(",")
410
- if "bold" in styles:
455
+ if "b" in styles:
411
456
  charFormat.setFontWeight(QFont.Weight.Bold)
412
- if "italic" in styles:
457
+ if "i" in styles:
413
458
  charFormat.setFontItalic(True)
414
- if "strike" in styles:
459
+ if "s" in styles:
415
460
  charFormat.setFontStrikeOut(True)
416
- if "errline" in styles:
461
+ if "err" in styles:
417
462
  charFormat.setUnderlineColor(SHARED.theme.colError)
418
463
  charFormat.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline)
419
- if "background" in styles and color is not None:
464
+ if "bg" in styles and color is not None:
420
465
  charFormat.setBackground(QBrush(color, Qt.BrushStyle.SolidPattern))
421
466
 
422
- if size is not None:
423
- charFormat.setFontPointSize(int(round(size*CONFIG.textSize)))
467
+ if size:
468
+ charFormat.setFontPointSize(round(size*CONFIG.textFont.pointSize()))
424
469
 
425
- return charFormat
470
+ self._hStyles[name] = charFormat
426
471
 
427
- # END Class GuiDocHighlighter
472
+ return
428
473
 
429
474
 
430
475
  class TextBlockData(QTextBlockUserData):
@@ -441,14 +486,14 @@ class TextBlockData(QTextBlockUserData):
441
486
  """Return spell error data from last check."""
442
487
  return self._spellErrors
443
488
 
444
- def spellCheck(self, text: str) -> list[tuple[int, int]]:
489
+ def spellCheck(self, text: str, offset: int) -> list[tuple[int, int]]:
445
490
  """Run the spell checker and cache the result, and return the
446
491
  list of spell check errors.
447
492
  """
448
493
  if "[" in text:
449
494
  # Strip shortcodes
450
495
  for rX in [SPELLSC, SPELLSV]:
451
- rxItt = rX.globalMatch(text, 0)
496
+ rxItt = rX.globalMatch(text, offset)
452
497
  while rxItt.hasNext():
453
498
  rxMatch = rxItt.next()
454
499
  xPos = rxMatch.capturedStart(0)
@@ -457,12 +502,12 @@ class TextBlockData(QTextBlockUserData):
457
502
  text = text[:xPos] + " "*xLen + text[xEnd:]
458
503
 
459
504
  self._spellErrors = []
460
- rxSpell = SPELLRX.globalMatch(text.replace("_", " "), 0)
505
+ rxSpell = SPELLRX.globalMatch(text.replace("_", " "), offset)
461
506
  while rxSpell.hasNext():
462
507
  rxMatch = rxSpell.next()
463
508
  if not SHARED.spelling.checkWord(rxMatch.captured(0)):
464
509
  if not rxMatch.captured(0).isnumeric() and not rxMatch.captured(0).isupper():
465
- self._spellErrors.append((rxMatch.capturedStart(0), rxMatch.capturedLength(0)))
510
+ self._spellErrors.append(
511
+ (rxMatch.capturedStart(0), rxMatch.capturedLength(0))
512
+ )
466
513
  return self._spellErrors
467
-
468
- # END Class TextBlockData