novelWriter 2.4.3__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 (123) hide show
  1. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/RECORD +122 -112
  3. {novelWriter-2.4.3.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/i18n/project_pt_BR.json +74 -74
  19. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  20. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  21. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  22. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  23. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  24. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  25. novelwriter/assets/manual.pdf +0 -0
  26. novelwriter/assets/sample.zip +0 -0
  27. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  28. novelwriter/assets/syntax/default_dark.conf +32 -18
  29. novelwriter/assets/syntax/default_light.conf +24 -10
  30. novelwriter/assets/syntax/dracula.conf +44 -0
  31. novelwriter/assets/syntax/grey_dark.conf +5 -4
  32. novelwriter/assets/syntax/grey_light.conf +5 -4
  33. novelwriter/assets/syntax/light_owl.conf +7 -6
  34. novelwriter/assets/syntax/night_owl.conf +7 -6
  35. novelwriter/assets/syntax/snazzy.conf +42 -0
  36. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  37. novelwriter/assets/syntax/solarized_light.conf +4 -3
  38. novelwriter/assets/syntax/tango.conf +27 -11
  39. novelwriter/assets/syntax/tomorrow.conf +6 -5
  40. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  41. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  42. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  43. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  44. novelwriter/assets/text/credits_en.htm +52 -41
  45. novelwriter/assets/themes/cyberpunk_night.conf +3 -0
  46. novelwriter/assets/themes/default_dark.conf +2 -0
  47. novelwriter/assets/themes/default_light.conf +2 -0
  48. novelwriter/assets/themes/dracula.conf +48 -0
  49. novelwriter/assets/themes/solarized_dark.conf +2 -0
  50. novelwriter/assets/themes/solarized_light.conf +2 -0
  51. novelwriter/common.py +33 -12
  52. novelwriter/config.py +184 -98
  53. novelwriter/constants.py +47 -35
  54. novelwriter/core/buildsettings.py +68 -69
  55. novelwriter/core/coretools.py +5 -23
  56. novelwriter/core/docbuild.py +52 -40
  57. novelwriter/core/document.py +3 -5
  58. novelwriter/core/index.py +115 -45
  59. novelwriter/core/item.py +8 -19
  60. novelwriter/core/options.py +2 -4
  61. novelwriter/core/project.py +37 -61
  62. novelwriter/core/projectdata.py +1 -3
  63. novelwriter/core/projectxml.py +12 -15
  64. novelwriter/core/sessions.py +3 -5
  65. novelwriter/core/spellcheck.py +4 -9
  66. novelwriter/core/status.py +211 -164
  67. novelwriter/core/storage.py +0 -8
  68. novelwriter/core/tohtml.py +139 -105
  69. novelwriter/core/tokenizer.py +278 -122
  70. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  71. novelwriter/core/toodt.py +257 -166
  72. novelwriter/core/toqdoc.py +419 -0
  73. novelwriter/core/tree.py +5 -7
  74. novelwriter/dialogs/about.py +11 -18
  75. novelwriter/dialogs/docmerge.py +17 -19
  76. novelwriter/dialogs/docsplit.py +17 -19
  77. novelwriter/dialogs/editlabel.py +6 -10
  78. novelwriter/dialogs/preferences.py +200 -164
  79. novelwriter/dialogs/projectsettings.py +225 -189
  80. novelwriter/dialogs/quotes.py +12 -9
  81. novelwriter/dialogs/wordlist.py +9 -15
  82. novelwriter/enum.py +35 -30
  83. novelwriter/error.py +8 -15
  84. novelwriter/extensions/configlayout.py +55 -21
  85. novelwriter/extensions/eventfilters.py +1 -5
  86. novelwriter/extensions/modified.py +70 -14
  87. novelwriter/extensions/novelselector.py +1 -3
  88. novelwriter/extensions/pagedsidebar.py +9 -12
  89. novelwriter/extensions/{circularprogress.py → progressbars.py} +30 -8
  90. novelwriter/extensions/statusled.py +40 -26
  91. novelwriter/extensions/switch.py +4 -6
  92. novelwriter/extensions/switchbox.py +7 -6
  93. novelwriter/extensions/versioninfo.py +3 -9
  94. novelwriter/gui/doceditor.py +120 -139
  95. novelwriter/gui/dochighlight.py +231 -186
  96. novelwriter/gui/docviewer.py +69 -108
  97. novelwriter/gui/docviewerpanel.py +3 -10
  98. novelwriter/gui/editordocument.py +1 -3
  99. novelwriter/gui/itemdetails.py +7 -11
  100. novelwriter/gui/mainmenu.py +22 -18
  101. novelwriter/gui/noveltree.py +11 -24
  102. novelwriter/gui/outline.py +15 -26
  103. novelwriter/gui/projtree.py +39 -65
  104. novelwriter/gui/search.py +10 -3
  105. novelwriter/gui/sidebar.py +2 -6
  106. novelwriter/gui/statusbar.py +29 -37
  107. novelwriter/gui/theme.py +26 -48
  108. novelwriter/guimain.py +162 -160
  109. novelwriter/shared.py +36 -19
  110. novelwriter/text/patterns.py +113 -0
  111. novelwriter/tools/dictionaries.py +10 -20
  112. novelwriter/tools/lipsum.py +10 -16
  113. novelwriter/tools/manusbuild.py +9 -11
  114. novelwriter/tools/manuscript.py +75 -149
  115. novelwriter/tools/manussettings.py +74 -76
  116. novelwriter/tools/noveldetails.py +16 -21
  117. novelwriter/tools/welcome.py +21 -26
  118. novelwriter/tools/writingstats.py +9 -12
  119. novelwriter/types.py +49 -4
  120. novelwriter/extensions/simpleprogress.py +0 -55
  121. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/LICENSE.md +0 -0
  122. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/entry_points.txt +0 -0
  123. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/top_level.txt +0 -0
novelwriter/core/toodt.py CHANGED
@@ -29,17 +29,20 @@ from __future__ import annotations
29
29
  import logging
30
30
  import xml.etree.ElementTree as ET
31
31
 
32
+ from collections.abc import Sequence
33
+ from datetime import datetime
32
34
  from hashlib import sha256
33
35
  from pathlib import Path
34
36
  from zipfile import ZipFile
35
- from datetime import datetime
36
- from collections.abc import Sequence
37
+
38
+ from PyQt5.QtGui import QFont
37
39
 
38
40
  from novelwriter import __version__
39
41
  from novelwriter.common import xmlIndent
40
42
  from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels
41
43
  from novelwriter.core.project import NWProject
42
- from novelwriter.core.tokenizer import Tokenizer, stripEscape
44
+ from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
45
+ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
43
46
 
44
47
  logger = logging.getLogger(__name__)
45
48
 
@@ -79,13 +82,15 @@ TAG_SPAN = _mkTag("text", "span")
79
82
  TAG_STNM = _mkTag("text", "style-name")
80
83
 
81
84
  # Formatting Codes
82
- X_BLD = 0x01 # Bold format
83
- X_ITA = 0x02 # Italic format
84
- X_DEL = 0x04 # Strikethrough format
85
- X_UND = 0x08 # Underline format
86
- X_MRK = 0x10 # Marked format
87
- X_SUP = 0x20 # Superscript
88
- X_SUB = 0x40 # Subscript
85
+ X_BLD = 0x001 # Bold format
86
+ X_ITA = 0x002 # Italic format
87
+ X_DEL = 0x004 # Strikethrough format
88
+ X_UND = 0x008 # Underline format
89
+ X_MRK = 0x010 # Marked format
90
+ X_SUP = 0x020 # Superscript
91
+ X_SUB = 0x040 # Subscript
92
+ X_DLG = 0x080 # Dialogue
93
+ X_DLA = 0x100 # Alt. Dialogue
89
94
 
90
95
  # Formatting Masks
91
96
  M_BLD = ~X_BLD
@@ -95,6 +100,24 @@ M_UND = ~X_UND
95
100
  M_MRK = ~X_MRK
96
101
  M_SUP = ~X_SUP
97
102
  M_SUB = ~X_SUB
103
+ M_DLG = ~X_DLG
104
+ M_DLA = ~X_DLA
105
+
106
+ # ODT Styles
107
+ S_TITLE = "Title"
108
+ S_HEAD1 = "Heading_20_1"
109
+ S_HEAD2 = "Heading_20_2"
110
+ S_HEAD3 = "Heading_20_3"
111
+ S_HEAD4 = "Heading_20_4"
112
+ S_SEP = "Separator"
113
+ S_FIND = "First_20_line_20_indent"
114
+ S_TEXT = "Text_20_body"
115
+ S_META = "Text_20_Meta"
116
+ S_HNF = "Header_20_and_20_Footer"
117
+
118
+ # Font Data
119
+ FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
120
+ FONT_WEIGHT_MAP = {"400": "normal", "700": "bold"}
98
121
 
99
122
 
100
123
  class ToOdt(Tokenizer):
@@ -130,20 +153,25 @@ class ToOdt(Tokenizer):
130
153
  self._autoPara: dict[str, ODTParagraphStyle] = {} # Auto-generated paragraph styles
131
154
  self._autoText: dict[int, ODTTextStyle] = {} # Auto-generated text styles
132
155
 
156
+ # Footnotes
157
+ self._nNote = 0
158
+ self._etNotes: dict[str, ET.Element] = {} # Generated note elements
159
+
133
160
  self._errData = [] # List of errors encountered
134
161
 
135
162
  # Properties
136
- self._textFont = "Liberation Serif"
137
- self._textSize = 12
138
- self._textFixed = False
163
+ self._textFont = QFont("Liberation Serif", 12)
139
164
  self._colourHead = False
140
- self._firstIndent = False
141
165
  self._headerFormat = ""
142
166
  self._pageOffset = 0
143
167
 
144
168
  # Internal
145
- self._fontFamily = "'Liberation Serif'"
169
+ self._fontFamily = "Liberation Serif"
170
+ self._fontSize = 12
171
+ self._fontWeight = "normal"
172
+ self._fontStyle = "normal"
146
173
  self._fontPitch = "variable"
174
+ self._fontBold = "bold"
147
175
  self._fSizeTitle = "30pt"
148
176
  self._fSizeHead1 = "24pt"
149
177
  self._fSizeHead2 = "20pt"
@@ -151,6 +179,7 @@ class ToOdt(Tokenizer):
151
179
  self._fSizeHead4 = "14pt"
152
180
  self._fSizeHead = "14pt"
153
181
  self._fSizeText = "12pt"
182
+ self._fSizeFoot = "10pt"
154
183
  self._fLineHeight = "115%"
155
184
  self._fBlockIndent = "1.693cm"
156
185
  self._fTextIndent = "0.499cm"
@@ -167,6 +196,7 @@ class ToOdt(Tokenizer):
167
196
  self._mTopHead = "0.423cm"
168
197
  self._mTopText = "0.000cm"
169
198
  self._mTopMeta = "0.000cm"
199
+ self._mTopSep = "0.247cm"
170
200
 
171
201
  self._mBotTitle = "0.212cm"
172
202
  self._mBotHead1 = "0.212cm"
@@ -176,6 +206,10 @@ class ToOdt(Tokenizer):
176
206
  self._mBotHead = "0.212cm"
177
207
  self._mBotText = "0.247cm"
178
208
  self._mBotMeta = "0.106cm"
209
+ self._mBotSep = "0.247cm"
210
+
211
+ self._mBotFoot = "0.106cm"
212
+ self._mLeftFoot = "0.600cm"
179
213
 
180
214
  # Document Size and Margins
181
215
  self._mDocWidth = "21.0cm"
@@ -186,13 +220,15 @@ class ToOdt(Tokenizer):
186
220
  self._mDocRight = "2.000cm"
187
221
 
188
222
  # Colour
189
- self._colHead12 = None
190
- self._opaHead12 = None
191
- self._colHead34 = None
192
- self._opaHead34 = None
193
- self._colMetaTx = None
194
- self._opaMetaTx = None
195
- self._markText = "#ffffa6"
223
+ self._colHead12 = None
224
+ self._opaHead12 = None
225
+ self._colHead34 = None
226
+ self._opaHead34 = None
227
+ self._colMetaTx = None
228
+ self._opaMetaTx = None
229
+ self._colDialogM = None
230
+ self._colDialogA = None
231
+ self._markText = "#ffffa6"
196
232
 
197
233
  return
198
234
 
@@ -232,11 +268,6 @@ class ToOdt(Tokenizer):
232
268
  self._pageOffset = offset
233
269
  return
234
270
 
235
- def setFirstLineIndent(self, state: bool) -> None:
236
- """Enable or disable first line indent."""
237
- self._firstIndent = state
238
- return
239
-
240
271
  ##
241
272
  # Class Methods
242
273
  ##
@@ -246,18 +277,25 @@ class ToOdt(Tokenizer):
246
277
  # Initialise Variables
247
278
  # ====================
248
279
 
249
- self._fontFamily = self._textFont
250
- if len(self._textFont.split()) > 1:
251
- self._fontFamily = f"'{self._textFont}'"
252
- self._fontPitch = "fixed" if self._textFixed else "variable"
253
-
254
- self._fSizeTitle = f"{round(2.50 * self._textSize):d}pt"
255
- self._fSizeHead1 = f"{round(2.00 * self._textSize):d}pt"
256
- self._fSizeHead2 = f"{round(1.60 * self._textSize):d}pt"
257
- self._fSizeHead3 = f"{round(1.30 * self._textSize):d}pt"
258
- self._fSizeHead4 = f"{round(1.15 * self._textSize):d}pt"
259
- self._fSizeHead = f"{round(1.15 * self._textSize):d}pt"
260
- self._fSizeText = f"{self._textSize:d}pt"
280
+ intWeight = FONT_WEIGHTS.get(self._textFont.weight(), 400)
281
+ fontWeight = str(intWeight)
282
+ fontBold = str(min(intWeight + 300, 900))
283
+
284
+ self._fontFamily = self._textFont.family()
285
+ self._fontSize = self._textFont.pointSize()
286
+ self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight)
287
+ self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal")
288
+ self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable"
289
+ self._fontBold = FONT_WEIGHT_MAP.get(fontBold, fontBold)
290
+
291
+ self._fSizeTitle = f"{round(2.50 * self._fontSize):d}pt"
292
+ self._fSizeHead1 = f"{round(2.00 * self._fontSize):d}pt"
293
+ self._fSizeHead2 = f"{round(1.60 * self._fontSize):d}pt"
294
+ self._fSizeHead3 = f"{round(1.30 * self._fontSize):d}pt"
295
+ self._fSizeHead4 = f"{round(1.15 * self._fontSize):d}pt"
296
+ self._fSizeHead = f"{round(1.15 * self._fontSize):d}pt"
297
+ self._fSizeText = f"{self._fontSize:d}pt"
298
+ self._fSizeFoot = f"{round(0.8*self._fontSize):d}pt"
261
299
 
262
300
  mScale = self._lineHeight/1.15
263
301
 
@@ -269,6 +307,7 @@ class ToOdt(Tokenizer):
269
307
  self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
270
308
  self._mTopText = self._emToCm(mScale * self._marginText[0])
271
309
  self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
310
+ self._mTopSep = self._emToCm(mScale * self._marginSep[0])
272
311
 
273
312
  self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
274
313
  self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
@@ -278,6 +317,10 @@ class ToOdt(Tokenizer):
278
317
  self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
279
318
  self._mBotText = self._emToCm(mScale * self._marginText[1])
280
319
  self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
320
+ self._mBotSep = self._emToCm(mScale * self._marginSep[1])
321
+
322
+ self._mLeftFoot = self._emToCm(self._marginFoot[0])
323
+ self._mBotFoot = self._emToCm(self._marginFoot[1])
281
324
 
282
325
  if self._colourHead:
283
326
  self._colHead12 = "#2a6099"
@@ -287,8 +330,13 @@ class ToOdt(Tokenizer):
287
330
  self._colMetaTx = "#813709"
288
331
  self._opaMetaTx = "100%"
289
332
 
333
+ if self._showDialog:
334
+ self._colDialogM = "#2a6099"
335
+ self._colDialogA = "#813709"
336
+
290
337
  self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
291
338
  self._fBlockIndent = self._emToCm(self._blockIndent)
339
+ self._fTextIndent = self._emToCm(self._firstWidth)
292
340
  self._textAlign = "justify" if self._doJustify else "left"
293
341
 
294
342
  # Clear Errors
@@ -301,7 +349,7 @@ class ToOdt(Tokenizer):
301
349
  tAttr[_mkTag("office", "version")] = X_VERS
302
350
 
303
351
  fAttr = {}
304
- fAttr[_mkTag("style", "name")] = self._textFont
352
+ fAttr[_mkTag("style", "name")] = self._fontFamily
305
353
  fAttr[_mkTag("style", "font-pitch")] = self._fontPitch
306
354
 
307
355
  if self._isFlat:
@@ -399,10 +447,7 @@ class ToOdt(Tokenizer):
399
447
  """Convert the list of text tokens into XML elements."""
400
448
  self._result = "" # Not used, but cleared just in case
401
449
 
402
- pFmt = []
403
- pText = []
404
- pStyle = None
405
- pIndent = True
450
+ xText = self._xText
406
451
  for tType, _, tText, tFormat, tStyle in self._tokens:
407
452
 
408
453
  # Styles
@@ -419,7 +464,6 @@ class ToOdt(Tokenizer):
419
464
 
420
465
  if tStyle & self.A_PBB:
421
466
  oStyle.setBreakBefore("page")
422
-
423
467
  if tStyle & self.A_PBA:
424
468
  oStyle.setBreakAfter("page")
425
469
 
@@ -433,82 +477,57 @@ class ToOdt(Tokenizer):
433
477
  if tStyle & self.A_IND_R:
434
478
  oStyle.setMarginRight(self._fBlockIndent)
435
479
 
436
- if tType not in (self.T_EMPTY, self.T_TEXT):
437
- pIndent = False
438
-
439
480
  # Process Text Types
440
- if tType == self.T_EMPTY:
441
- if len(pText) > 1 and pStyle is not None:
442
- if self._doJustify:
443
- pStyle.setTextAlign("left")
444
-
445
- if len(pText) > 0 and pStyle is not None:
446
- tTxt = ""
447
- tFmt = []
448
- for nText, nFmt in zip(pText, pFmt):
449
- tLen = len(tTxt)
450
- tTxt += f"{nText}\n"
451
- tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
452
-
453
- # Don't indent a paragraph if it has alignment set
454
- tIndent = self._firstIndent and pIndent and pStyle.isUnaligned()
455
- self._addTextPar(
456
- "First_20_line_20_indent" if tIndent else "Text_20_body",
457
- pStyle, tTxt.rstrip(), tFmt=tFmt
458
- )
459
- pIndent = True
460
-
461
- pFmt = []
462
- pText = []
463
- pStyle = None
481
+ if tType == self.T_TEXT:
482
+ # Text indentation is processed here because there is a
483
+ # dedicated pre-defined style for it
484
+ if tStyle & self.A_IND_T:
485
+ self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
486
+ else:
487
+ self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
464
488
 
465
489
  elif tType == self.T_TITLE:
490
+ # Title must be text:p
466
491
  tHead = tText.replace(nwHeadFmt.BR, "\n")
467
- self._addTextPar("Title", oStyle, tHead, isHead=False) # Title must be text:p
492
+ self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
468
493
 
469
494
  elif tType == self.T_HEAD1:
470
495
  tHead = tText.replace(nwHeadFmt.BR, "\n")
471
- self._addTextPar("Heading_20_1", oStyle, tHead, isHead=True, oLevel="1")
496
+ self._addTextPar(xText, S_HEAD1, oStyle, tHead, isHead=True, oLevel="1")
472
497
 
473
498
  elif tType == self.T_HEAD2:
474
499
  tHead = tText.replace(nwHeadFmt.BR, "\n")
475
- self._addTextPar("Heading_20_2", oStyle, tHead, isHead=True, oLevel="2")
500
+ self._addTextPar(xText, S_HEAD2, oStyle, tHead, isHead=True, oLevel="2")
476
501
 
477
502
  elif tType == self.T_HEAD3:
478
503
  tHead = tText.replace(nwHeadFmt.BR, "\n")
479
- self._addTextPar("Heading_20_3", oStyle, tHead, isHead=True, oLevel="3")
504
+ self._addTextPar(xText, S_HEAD3, oStyle, tHead, isHead=True, oLevel="3")
480
505
 
481
506
  elif tType == self.T_HEAD4:
482
507
  tHead = tText.replace(nwHeadFmt.BR, "\n")
483
- self._addTextPar("Heading_20_4", oStyle, tHead, isHead=True, oLevel="4")
508
+ self._addTextPar(xText, S_HEAD4, oStyle, tHead, isHead=True, oLevel="4")
484
509
 
485
510
  elif tType == self.T_SEP:
486
- self._addTextPar("Separator", oStyle, tText)
511
+ self._addTextPar(xText, S_SEP, oStyle, tText)
487
512
 
488
513
  elif tType == self.T_SKIP:
489
- self._addTextPar("Separator", oStyle, "")
490
-
491
- elif tType == self.T_TEXT:
492
- if pStyle is None:
493
- pStyle = oStyle
494
- pText.append(tText)
495
- pFmt.append(tFormat)
514
+ self._addTextPar(xText, S_TEXT, oStyle, "")
496
515
 
497
516
  elif tType == self.T_SYNOPSIS and self._doSynopsis:
498
- tTemp, fTemp = self._formatSynopsis(tText, True)
499
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
517
+ tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
518
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
500
519
 
501
520
  elif tType == self.T_SHORT and self._doSynopsis:
502
- tTemp, fTemp = self._formatSynopsis(tText, False)
503
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
521
+ tTemp, tFmt = self._formatSynopsis(tText, tFormat, False)
522
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
504
523
 
505
524
  elif tType == self.T_COMMENT and self._doComments:
506
- tTemp, fTemp = self._formatComments(tText)
507
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
525
+ tTemp, tFmt = self._formatComments(tText, tFormat)
526
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
508
527
 
509
528
  elif tType == self.T_KEYWORD and self._doKeywords:
510
- tTemp, fTemp = self._formatKeywords(tText)
511
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
529
+ tTemp, tFmt = self._formatKeywords(tText)
530
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
512
531
 
513
532
  return
514
533
 
@@ -569,28 +588,32 @@ class ToOdt(Tokenizer):
569
588
  # Internal Functions
570
589
  ##
571
590
 
572
- def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
591
+ def _formatSynopsis(self, text: str, fmt: T_Formats, synopsis: bool) -> tuple[str, T_Formats]:
573
592
  """Apply formatting to synopsis lines."""
574
593
  name = self._localLookup("Synopsis" if synopsis else "Short Description")
594
+ shift = len(name) + 2
575
595
  rTxt = f"{name}: {text}"
576
- rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
596
+ rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(name) + 1, self.FMT_B_E, "")]
597
+ rFmt.extend((p + shift, f, d) for p, f, d in fmt)
577
598
  return rTxt, rFmt
578
599
 
579
- def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
600
+ def _formatComments(self, text: str, fmt: T_Formats) -> tuple[str, T_Formats]:
580
601
  """Apply formatting to comments."""
581
602
  name = self._localLookup("Comment")
603
+ shift = len(name) + 2
582
604
  rTxt = f"{name}: {text}"
583
- rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
605
+ rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(name) + 1, self.FMT_B_E, "")]
606
+ rFmt.extend((p + shift, f, d) for p, f, d in fmt)
584
607
  return rTxt, rFmt
585
608
 
586
- def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
609
+ def _formatKeywords(self, text: str) -> tuple[str, T_Formats]:
587
610
  """Apply formatting to keywords."""
588
611
  valid, bits, _ = self._project.index.scanThis("@"+text)
589
612
  if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
590
613
  return "", []
591
614
 
592
615
  rTxt = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
593
- rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
616
+ rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(rTxt) - 1, self.FMT_B_E, "")]
594
617
  if len(bits) > 1:
595
618
  if bits[0] == nwKeyWords.TAG_KEY:
596
619
  rTxt += bits[1]
@@ -600,8 +623,8 @@ class ToOdt(Tokenizer):
600
623
  return rTxt, rFmt
601
624
 
602
625
  def _addTextPar(
603
- self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
604
- tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
626
+ self, xParent: ET.Element, styleName: str, oStyle: ODTParagraphStyle, tText: str,
627
+ tFmt: Sequence[tuple[int, int, str]] = [], isHead: bool = False, oLevel: str | None = None
605
628
  ) -> None:
606
629
  """Add a text paragraph to the text XML element."""
607
630
  tAttr = {_mkTag("text", "style-name"): self._paraStyle(styleName, oStyle)}
@@ -609,7 +632,7 @@ class ToOdt(Tokenizer):
609
632
  tAttr[_mkTag("text", "outline-level")] = oLevel
610
633
 
611
634
  pTag = "h" if isHead else "p"
612
- xElem = ET.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
635
+ xElem = ET.SubElement(xParent, _mkTag("text", pTag), attrib=tAttr)
613
636
 
614
637
  # It's important to set the initial text field to empty, otherwise
615
638
  # xmlIndent will add a line break if the first subelement is a span.
@@ -627,7 +650,13 @@ class ToOdt(Tokenizer):
627
650
  xFmt = 0x00
628
651
  tFrag = ""
629
652
  fLast = 0
630
- for fPos, fFmt in tFmt:
653
+ xNode = None
654
+ for fPos, fFmt, fData in tFmt:
655
+
656
+ # Add any extra nodes
657
+ if xNode is not None:
658
+ parProc.appendNode(xNode)
659
+ xNode = None
631
660
 
632
661
  # Add the text up to the current fragment
633
662
  if tFrag := tText[fLast:fPos]:
@@ -665,11 +694,26 @@ class ToOdt(Tokenizer):
665
694
  xFmt |= X_SUB
666
695
  elif fFmt == self.FMT_SUB_E:
667
696
  xFmt &= M_SUB
697
+ elif fFmt == self.FMT_DL_B:
698
+ xFmt |= X_DLG
699
+ elif fFmt == self.FMT_DL_E:
700
+ xFmt &= M_DLG
701
+ elif fFmt == self.FMT_ADL_B:
702
+ xFmt |= X_DLA
703
+ elif fFmt == self.FMT_ADL_E:
704
+ xFmt &= M_DLA
705
+ elif fFmt == self.FMT_FNOTE:
706
+ xNode = self._generateFootnote(fData)
707
+ elif fFmt == self.FMT_STRIP:
708
+ pass
668
709
  else:
669
710
  pErr += 1
670
711
 
671
712
  fLast = fPos
672
713
 
714
+ if xNode is not None:
715
+ parProc.appendNode(xNode)
716
+
673
717
  if tFrag := tText[fLast:]:
674
718
  if xFmt == 0x00:
675
719
  parProc.appendText(tFrag)
@@ -715,7 +759,7 @@ class ToOdt(Tokenizer):
715
759
 
716
760
  style = ODTTextStyle(f"T{len(self._autoText)+1:d}")
717
761
  if hFmt & X_BLD:
718
- style.setFontWeight("bold")
762
+ style.setFontWeight(self._fontBold)
719
763
  if hFmt & X_ITA:
720
764
  style.setFontStyle("italic")
721
765
  if hFmt & X_DEL:
@@ -731,13 +775,33 @@ class ToOdt(Tokenizer):
731
775
  style.setTextPosition("super")
732
776
  if hFmt & X_SUB:
733
777
  style.setTextPosition("sub")
778
+ if hFmt & X_DLG:
779
+ style.setColour(self._colDialogM)
780
+ if hFmt & X_DLA:
781
+ style.setColour(self._colDialogA)
734
782
  self._autoText[hFmt] = style
735
783
 
736
784
  return style.name
737
785
 
786
+ def _generateFootnote(self, key: str) -> ET.Element | None:
787
+ """Generate a footnote XML object."""
788
+ if content := self._footnotes.get(key):
789
+ self._nNote += 1
790
+ nStyle = ODTParagraphStyle("New")
791
+ xNote = ET.Element(_mkTag("text", "note"), attrib={
792
+ _mkTag("text", "id"): f"ftn{self._nNote}",
793
+ _mkTag("text", "note-class"): "footnote",
794
+ })
795
+ xCite = ET.SubElement(xNote, _mkTag("text", "note-citation"))
796
+ xCite.text = str(self._nNote)
797
+ xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
798
+ self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
799
+ return xNote
800
+ return None
801
+
738
802
  def _emToCm(self, value: float) -> str:
739
803
  """Converts an em value to centimetres."""
740
- return f"{value*2.54/72*self._textSize:.3f}cm"
804
+ return f"{value*2.54/72*self._fontSize:.3f}cm"
741
805
 
742
806
  ##
743
807
  # Style Elements
@@ -757,7 +821,6 @@ class ToOdt(Tokenizer):
757
821
  _mkTag("fo", "margin-bottom"): self._mDocBtm,
758
822
  _mkTag("fo", "margin-left"): self._mDocLeft,
759
823
  _mkTag("fo", "margin-right"): self._mDocRight,
760
- _mkTag("fo", "print-orientation"): "portrait",
761
824
  })
762
825
 
763
826
  xHead = ET.SubElement(xPage, _mkTag("style", "header-style"))
@@ -782,8 +845,10 @@ class ToOdt(Tokenizer):
782
845
  _mkTag("style", "writing-mode"): "page",
783
846
  })
784
847
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
785
- _mkTag("style", "font-name"): self._textFont,
848
+ _mkTag("style", "font-name"): self._fontFamily,
786
849
  _mkTag("fo", "font-family"): self._fontFamily,
850
+ _mkTag("fo", "font-weight"): self._fontWeight,
851
+ _mkTag("fo", "font-style"): self._fontStyle,
787
852
  _mkTag("fo", "font-size"): self._fSizeText,
788
853
  _mkTag("fo", "language"): self._dLanguage,
789
854
  _mkTag("fo", "country"): self._dCountry,
@@ -796,8 +861,10 @@ class ToOdt(Tokenizer):
796
861
  _mkTag("style", "class"): "text",
797
862
  })
798
863
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
799
- _mkTag("style", "font-name"): self._textFont,
864
+ _mkTag("style", "font-name"): self._fontFamily,
800
865
  _mkTag("fo", "font-family"): self._fontFamily,
866
+ _mkTag("fo", "font-weight"): self._fontWeight,
867
+ _mkTag("fo", "font-style"): self._fontStyle,
801
868
  _mkTag("fo", "font-size"): self._fSizeText,
802
869
  })
803
870
 
@@ -806,7 +873,7 @@ class ToOdt(Tokenizer):
806
873
  _mkTag("style", "name"): "Heading",
807
874
  _mkTag("style", "family"): "paragraph",
808
875
  _mkTag("style", "parent-style-name"): "Standard",
809
- _mkTag("style", "next-style-name"): "Text_20_body",
876
+ _mkTag("style", "next-style-name"): S_TEXT,
810
877
  _mkTag("style", "class"): "text",
811
878
  })
812
879
  ET.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib={
@@ -815,14 +882,16 @@ class ToOdt(Tokenizer):
815
882
  _mkTag("fo", "keep-with-next"): "always",
816
883
  })
817
884
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
818
- _mkTag("style", "font-name"): self._textFont,
885
+ _mkTag("style", "font-name"): self._fontFamily,
819
886
  _mkTag("fo", "font-family"): self._fontFamily,
887
+ _mkTag("fo", "font-weight"): self._fontWeight,
888
+ _mkTag("fo", "font-style"): self._fontStyle,
820
889
  _mkTag("fo", "font-size"): self._fSizeHead,
821
890
  })
822
891
 
823
892
  # Add Header and Footer Styles
824
893
  ET.SubElement(self._xStyl, _mkTag("style", "style"), attrib={
825
- _mkTag("style", "name"): "Header_20_and_20_Footer",
894
+ _mkTag("style", "name"): S_HNF,
826
895
  _mkTag("style", "display-name"): "Header and Footer",
827
896
  _mkTag("style", "family"): "paragraph",
828
897
  _mkTag("style", "parent-style-name"): "Standard",
@@ -834,7 +903,7 @@ class ToOdt(Tokenizer):
834
903
  def _useableStyles(self) -> None:
835
904
  """Set the usable styles."""
836
905
  # Add Text Body Style
837
- style = ODTParagraphStyle("Text_20_body")
906
+ style = ODTParagraphStyle(S_TEXT)
838
907
  style.setDisplayName("Text body")
839
908
  style.setParentStyleName("Standard")
840
909
  style.setClass("text")
@@ -842,136 +911,139 @@ class ToOdt(Tokenizer):
842
911
  style.setMarginBottom(self._mBotText)
843
912
  style.setLineHeight(self._fLineHeight)
844
913
  style.setTextAlign(self._textAlign)
845
- style.setFontName(self._textFont)
914
+ style.setFontName(self._fontFamily)
846
915
  style.setFontFamily(self._fontFamily)
847
916
  style.setFontSize(self._fSizeText)
917
+ style.setFontWeight(self._fontWeight)
848
918
  style.packXML(self._xStyl)
849
919
  self._mainPara[style.name] = style
850
920
 
851
921
  # Add First Line Indent Style
852
- style = ODTParagraphStyle("First_20_line_20_indent")
922
+ style = ODTParagraphStyle(S_FIND)
853
923
  style.setDisplayName("First line indent")
854
- style.setParentStyleName("Text_20_body")
924
+ style.setParentStyleName(S_TEXT)
855
925
  style.setClass("text")
856
926
  style.setTextIndent(self._fTextIndent)
857
927
  style.packXML(self._xStyl)
858
928
  self._mainPara[style.name] = style
859
929
 
860
930
  # Add Text Meta Style
861
- style = ODTParagraphStyle("Text_20_Meta")
931
+ style = ODTParagraphStyle(S_META)
862
932
  style.setDisplayName("Text Meta")
863
933
  style.setParentStyleName("Standard")
864
934
  style.setClass("text")
865
935
  style.setMarginTop(self._mTopMeta)
866
936
  style.setMarginBottom(self._mBotMeta)
867
937
  style.setLineHeight(self._fLineHeight)
868
- style.setFontName(self._textFont)
938
+ style.setFontName(self._fontFamily)
869
939
  style.setFontFamily(self._fontFamily)
870
940
  style.setFontSize(self._fSizeText)
941
+ style.setFontWeight(self._fontWeight)
871
942
  style.setColour(self._colMetaTx)
872
943
  style.setOpacity(self._opaMetaTx)
873
944
  style.packXML(self._xStyl)
874
945
  self._mainPara[style.name] = style
875
946
 
876
947
  # Add Title Style
877
- style = ODTParagraphStyle("Title")
948
+ style = ODTParagraphStyle(S_TITLE)
878
949
  style.setDisplayName("Title")
879
950
  style.setParentStyleName("Heading")
880
- style.setNextStyleName("Text_20_body")
951
+ style.setNextStyleName(S_TEXT)
881
952
  style.setClass("chapter")
882
953
  style.setMarginTop(self._mTopTitle)
883
954
  style.setMarginBottom(self._mBotTitle)
884
955
  style.setTextAlign("center")
885
- style.setFontName(self._textFont)
956
+ style.setFontName(self._fontFamily)
886
957
  style.setFontFamily(self._fontFamily)
887
958
  style.setFontSize(self._fSizeTitle)
888
- style.setFontWeight("bold")
959
+ style.setFontWeight(self._fontBold)
889
960
  style.packXML(self._xStyl)
890
961
  self._mainPara[style.name] = style
891
962
 
892
963
  # Add Separator Style
893
- style = ODTParagraphStyle("Separator")
964
+ style = ODTParagraphStyle(S_SEP)
894
965
  style.setDisplayName("Separator")
895
966
  style.setParentStyleName("Standard")
896
- style.setNextStyleName("Text_20_body")
967
+ style.setNextStyleName(S_TEXT)
897
968
  style.setClass("text")
898
- style.setMarginTop(self._mTopText)
899
- style.setMarginBottom(self._mBotText)
969
+ style.setMarginTop(self._mTopSep)
970
+ style.setMarginBottom(self._mBotSep)
900
971
  style.setLineHeight(self._fLineHeight)
901
972
  style.setTextAlign("center")
902
- style.setFontName(self._textFont)
973
+ style.setFontName(self._fontFamily)
903
974
  style.setFontFamily(self._fontFamily)
904
975
  style.setFontSize(self._fSizeText)
976
+ style.setFontWeight(self._fontWeight)
905
977
  style.packXML(self._xStyl)
906
978
  self._mainPara[style.name] = style
907
979
 
908
980
  # Add Heading 1 Style
909
- style = ODTParagraphStyle("Heading_20_1")
981
+ style = ODTParagraphStyle(S_HEAD1)
910
982
  style.setDisplayName("Heading 1")
911
983
  style.setParentStyleName("Heading")
912
- style.setNextStyleName("Text_20_body")
984
+ style.setNextStyleName(S_TEXT)
913
985
  style.setOutlineLevel("1")
914
986
  style.setClass("text")
915
987
  style.setMarginTop(self._mTopHead1)
916
988
  style.setMarginBottom(self._mBotHead1)
917
- style.setFontName(self._textFont)
989
+ style.setFontName(self._fontFamily)
918
990
  style.setFontFamily(self._fontFamily)
919
991
  style.setFontSize(self._fSizeHead1)
920
- style.setFontWeight("bold")
992
+ style.setFontWeight(self._fontBold)
921
993
  style.setColour(self._colHead12)
922
994
  style.setOpacity(self._opaHead12)
923
995
  style.packXML(self._xStyl)
924
996
  self._mainPara[style.name] = style
925
997
 
926
998
  # Add Heading 2 Style
927
- style = ODTParagraphStyle("Heading_20_2")
999
+ style = ODTParagraphStyle(S_HEAD2)
928
1000
  style.setDisplayName("Heading 2")
929
1001
  style.setParentStyleName("Heading")
930
- style.setNextStyleName("Text_20_body")
1002
+ style.setNextStyleName(S_TEXT)
931
1003
  style.setOutlineLevel("2")
932
1004
  style.setClass("text")
933
1005
  style.setMarginTop(self._mTopHead2)
934
1006
  style.setMarginBottom(self._mBotHead2)
935
- style.setFontName(self._textFont)
1007
+ style.setFontName(self._fontFamily)
936
1008
  style.setFontFamily(self._fontFamily)
937
1009
  style.setFontSize(self._fSizeHead2)
938
- style.setFontWeight("bold")
1010
+ style.setFontWeight(self._fontBold)
939
1011
  style.setColour(self._colHead12)
940
1012
  style.setOpacity(self._opaHead12)
941
1013
  style.packXML(self._xStyl)
942
1014
  self._mainPara[style.name] = style
943
1015
 
944
1016
  # Add Heading 3 Style
945
- style = ODTParagraphStyle("Heading_20_3")
1017
+ style = ODTParagraphStyle(S_HEAD3)
946
1018
  style.setDisplayName("Heading 3")
947
1019
  style.setParentStyleName("Heading")
948
- style.setNextStyleName("Text_20_body")
1020
+ style.setNextStyleName(S_TEXT)
949
1021
  style.setOutlineLevel("3")
950
1022
  style.setClass("text")
951
1023
  style.setMarginTop(self._mTopHead3)
952
1024
  style.setMarginBottom(self._mBotHead3)
953
- style.setFontName(self._textFont)
1025
+ style.setFontName(self._fontFamily)
954
1026
  style.setFontFamily(self._fontFamily)
955
1027
  style.setFontSize(self._fSizeHead3)
956
- style.setFontWeight("bold")
1028
+ style.setFontWeight(self._fontBold)
957
1029
  style.setColour(self._colHead34)
958
1030
  style.setOpacity(self._opaHead34)
959
1031
  style.packXML(self._xStyl)
960
1032
  self._mainPara[style.name] = style
961
1033
 
962
1034
  # Add Heading 4 Style
963
- style = ODTParagraphStyle("Heading_20_4")
1035
+ style = ODTParagraphStyle(S_HEAD4)
964
1036
  style.setDisplayName("Heading 4")
965
1037
  style.setParentStyleName("Heading")
966
- style.setNextStyleName("Text_20_body")
1038
+ style.setNextStyleName(S_TEXT)
967
1039
  style.setOutlineLevel("4")
968
1040
  style.setClass("text")
969
1041
  style.setMarginTop(self._mTopHead4)
970
1042
  style.setMarginBottom(self._mBotHead4)
971
- style.setFontName(self._textFont)
1043
+ style.setFontName(self._fontFamily)
972
1044
  style.setFontFamily(self._fontFamily)
973
1045
  style.setFontSize(self._fSizeHead4)
974
- style.setFontWeight("bold")
1046
+ style.setFontWeight(self._fontBold)
975
1047
  style.setColour(self._colHead34)
976
1048
  style.setOpacity(self._opaHead34)
977
1049
  style.packXML(self._xStyl)
@@ -980,11 +1052,23 @@ class ToOdt(Tokenizer):
980
1052
  # Add Header Style
981
1053
  style = ODTParagraphStyle("Header")
982
1054
  style.setDisplayName("Header")
983
- style.setParentStyleName("Header_20_and_20_Footer")
1055
+ style.setParentStyleName(S_HNF)
984
1056
  style.setTextAlign("right")
985
1057
  style.packXML(self._xStyl)
986
1058
  self._mainPara[style.name] = style
987
1059
 
1060
+ # Add Footnote Style
1061
+ style = ODTParagraphStyle("Footnote")
1062
+ style.setDisplayName("Footnote")
1063
+ style.setParentStyleName("Standard")
1064
+ style.setClass("extra")
1065
+ style.setMarginLeft(self._mLeftFoot)
1066
+ style.setMarginBottom(self._mBotFoot)
1067
+ style.setTextIndent("-"+self._mLeftFoot)
1068
+ style.setFontSize(self._fSizeFoot)
1069
+ style.packXML(self._xStyl)
1070
+ self._mainPara[style.name] = style
1071
+
988
1072
  return
989
1073
 
990
1074
  def _writeHeader(self) -> None:
@@ -1026,12 +1110,9 @@ class ToOdt(Tokenizer):
1026
1110
 
1027
1111
  return
1028
1112
 
1029
- # END Class ToOdt
1030
-
1031
1113
 
1032
- # =============================================================================================== #
1033
- # Auto-Style Classes
1034
- # =============================================================================================== #
1114
+ # Auto-Style Classes
1115
+ # ==================
1035
1116
 
1036
1117
  class ODTParagraphStyle:
1037
1118
  """Wrapper class for the paragraph style setting used by the
@@ -1041,8 +1122,8 @@ class ODTParagraphStyle:
1041
1122
  VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
1042
1123
  VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
1043
1124
  VALID_LEVEL = ["1", "2", "3", "4"]
1044
- VALID_CLASS = ["text", "chapter"]
1045
- VALID_WEIGHT = ["normal", "inherit", "bold"]
1125
+ VALID_CLASS = ["text", "chapter", "extra"]
1126
+ VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM
1046
1127
 
1047
1128
  def __init__(self, name: str) -> None:
1048
1129
 
@@ -1279,16 +1360,14 @@ class ODTParagraphStyle:
1279
1360
 
1280
1361
  return
1281
1362
 
1282
- # END Class ODTParagraphStyle
1283
-
1284
1363
 
1285
1364
  class ODTTextStyle:
1286
1365
  """Wrapper class for the text style setting used by the exporter.
1287
1366
  Only the used settings are exposed here to keep the class minimal
1288
1367
  and fast.
1289
1368
  """
1290
- VALID_WEIGHT = ["normal", "inherit", "bold"]
1291
- VALID_STYLE = ["normal", "inherit", "italic"]
1369
+ VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM
1370
+ VALID_STYLE = ["normal", "italic", "oblique"]
1292
1371
  VALID_POS = ["super", "sub"]
1293
1372
  VALID_LSTYLE = ["none", "solid"]
1294
1373
  VALID_LTYPE = ["single", "double"]
@@ -1300,6 +1379,7 @@ class ODTTextStyle:
1300
1379
  self._tAttr = {
1301
1380
  "font-weight": ["fo", None],
1302
1381
  "font-style": ["fo", None],
1382
+ "color": ["fo", None],
1303
1383
  "background-color": ["fo", None],
1304
1384
  "text-position": ["style", None],
1305
1385
  "text-line-through-style": ["style", None],
@@ -1334,6 +1414,14 @@ class ODTTextStyle:
1334
1414
  self._tAttr["font-style"][1] = None
1335
1415
  return
1336
1416
 
1417
+ def setColour(self, value: str | None) -> None:
1418
+ """Set text colour."""
1419
+ if value and len(value) == 7 and value[0] == "#":
1420
+ self._tAttr["color"][1] = value
1421
+ else:
1422
+ self._tAttr["color"][1] = None
1423
+ return
1424
+
1337
1425
  def setBackgroundColour(self, value: str | None) -> None:
1338
1426
  """Set text background colour."""
1339
1427
  if value and len(value) == 7 and value[0] == "#":
@@ -1404,12 +1492,9 @@ class ODTTextStyle:
1404
1492
  ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
1405
1493
  return
1406
1494
 
1407
- # END Class ODTTextStyle
1408
-
1409
1495
 
1410
- # =============================================================================================== #
1411
- # XML Complex Element Helper Class
1412
- # =============================================================================================== #
1496
+ # XML Complex Element Helper Class
1497
+ # ================================
1413
1498
 
1414
1499
  X_ROOT_TEXT = 0
1415
1500
  X_ROOT_TAIL = 1
@@ -1464,7 +1549,6 @@ class XMLParagraph:
1464
1549
  if c == " ":
1465
1550
  nSpaces += 1
1466
1551
  continue
1467
-
1468
1552
  elif nSpaces > 0:
1469
1553
  self._processSpaces(nSpaces)
1470
1554
  nSpaces = 0
@@ -1475,26 +1559,22 @@ class XMLParagraph:
1475
1559
  self._xTail.tail = ""
1476
1560
  self._nState = X_ROOT_TAIL
1477
1561
  self._chrPos += 1
1478
-
1479
1562
  elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
1480
1563
  self._xSing = ET.SubElement(self._xTail, TAG_BR)
1481
1564
  self._xSing.tail = ""
1482
1565
  self._nState = X_SPAN_SING
1483
1566
  self._chrPos += 1
1484
-
1485
1567
  elif c == "\t":
1486
1568
  if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
1487
1569
  self._xTail = ET.SubElement(self._xRoot, TAG_TAB)
1488
1570
  self._xTail.tail = ""
1489
1571
  self._nState = X_ROOT_TAIL
1490
1572
  self._chrPos += 1
1491
-
1492
1573
  elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
1493
1574
  self._xSing = ET.SubElement(self._xTail, TAG_TAB)
1494
1575
  self._xSing.tail = ""
1495
1576
  self._chrPos += 1
1496
1577
  self._nState = X_SPAN_SING
1497
-
1498
1578
  else:
1499
1579
  if self._nState == X_ROOT_TEXT:
1500
1580
  self._xRoot.text = (self._xRoot.text or "") + c
@@ -1529,6 +1609,19 @@ class XMLParagraph:
1529
1609
  self._nState = X_ROOT_TAIL
1530
1610
  return
1531
1611
 
1612
+ def appendNode(self, xNode: ET.Element | None) -> None:
1613
+ """Append an XML node to the paragraph. We only check for the
1614
+ X_ROOT_TEXT and X_ROOT_TAIL states. X_SPAN_TEXT is not possible
1615
+ at all, and X_SPAN_SING only happens internally in an appendSpan
1616
+ call, returning us to an X_ROOT_TAIL state.
1617
+ """
1618
+ if xNode is not None and self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
1619
+ self._xRoot.append(xNode)
1620
+ self._xTail = xNode
1621
+ self._xTail.tail = ""
1622
+ self._nState = X_ROOT_TAIL
1623
+ return
1624
+
1532
1625
  def checkError(self) -> tuple[int, str]:
1533
1626
  """Check that the number of characters written matches the
1534
1627
  number of characters received.
@@ -1600,5 +1693,3 @@ class XMLParagraph:
1600
1693
  self._chrPos += nSpaces - 1
1601
1694
 
1602
1695
  return
1603
-
1604
- # END Class XMLParagraph