novelWriter 2.5.1__py3-none-any.whl → 2.6b1__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 (64) hide show
  1. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
  2. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
  3. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  5. novelwriter/assets/i18n/project_en_GB.json +1 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +1 -0
  9. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  10. novelwriter/assets/manual.pdf +0 -0
  11. novelwriter/assets/sample.zip +0 -0
  12. novelwriter/assets/themes/default_light.conf +2 -2
  13. novelwriter/common.py +63 -0
  14. novelwriter/config.py +10 -3
  15. novelwriter/constants.py +153 -60
  16. novelwriter/core/buildsettings.py +66 -39
  17. novelwriter/core/coretools.py +34 -22
  18. novelwriter/core/docbuild.py +130 -169
  19. novelwriter/core/index.py +29 -18
  20. novelwriter/core/item.py +2 -2
  21. novelwriter/core/options.py +4 -1
  22. novelwriter/core/spellcheck.py +9 -14
  23. novelwriter/dialogs/preferences.py +45 -32
  24. novelwriter/dialogs/projectsettings.py +3 -3
  25. novelwriter/enum.py +29 -23
  26. novelwriter/extensions/configlayout.py +24 -11
  27. novelwriter/extensions/modified.py +13 -1
  28. novelwriter/extensions/pagedsidebar.py +5 -5
  29. novelwriter/formats/shared.py +155 -0
  30. novelwriter/formats/todocx.py +1195 -0
  31. novelwriter/formats/tohtml.py +452 -0
  32. novelwriter/{core → formats}/tokenizer.py +483 -485
  33. novelwriter/formats/tomarkdown.py +217 -0
  34. novelwriter/{core → formats}/toodt.py +270 -320
  35. novelwriter/formats/toqdoc.py +436 -0
  36. novelwriter/formats/toraw.py +91 -0
  37. novelwriter/gui/doceditor.py +240 -193
  38. novelwriter/gui/dochighlight.py +96 -84
  39. novelwriter/gui/docviewer.py +56 -30
  40. novelwriter/gui/docviewerpanel.py +3 -3
  41. novelwriter/gui/editordocument.py +17 -2
  42. novelwriter/gui/itemdetails.py +8 -4
  43. novelwriter/gui/mainmenu.py +121 -60
  44. novelwriter/gui/noveltree.py +35 -37
  45. novelwriter/gui/outline.py +186 -238
  46. novelwriter/gui/projtree.py +142 -131
  47. novelwriter/gui/sidebar.py +7 -6
  48. novelwriter/gui/theme.py +5 -4
  49. novelwriter/guimain.py +43 -155
  50. novelwriter/shared.py +14 -4
  51. novelwriter/text/counting.py +2 -0
  52. novelwriter/text/patterns.py +155 -59
  53. novelwriter/tools/manusbuild.py +1 -1
  54. novelwriter/tools/manuscript.py +121 -78
  55. novelwriter/tools/manussettings.py +403 -260
  56. novelwriter/tools/welcome.py +4 -4
  57. novelwriter/tools/writingstats.py +3 -3
  58. novelwriter/types.py +16 -6
  59. novelwriter/core/tohtml.py +0 -530
  60. novelwriter/core/tomarkdown.py +0 -252
  61. novelwriter/core/toqdoc.py +0 -419
  62. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
  63. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
  64. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/top_level.txt +0 -0
@@ -33,29 +33,32 @@ from collections.abc import Sequence
33
33
  from datetime import datetime
34
34
  from hashlib import sha256
35
35
  from pathlib import Path
36
- from zipfile import ZipFile
36
+ from zipfile import ZIP_DEFLATED, ZipFile
37
37
 
38
- from PyQt5.QtGui import QFont
38
+ from PyQt5.QtGui import QColor, QFont
39
39
 
40
40
  from novelwriter import __version__
41
- from novelwriter.common import xmlIndent
42
- from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels
41
+ from novelwriter.common import xmlElement, xmlIndent, xmlSubElem
42
+ from novelwriter.constants import nwHeadFmt, nwStyles
43
43
  from novelwriter.core.project import NWProject
44
- from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
45
- from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
44
+ from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape
45
+ from novelwriter.formats.tokenizer import Tokenizer
46
+ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb
46
47
 
47
48
  logger = logging.getLogger(__name__)
48
49
 
49
50
  # Main XML NameSpaces
50
51
  XML_NS = {
52
+ "dc": "http://purl.org/dc/elements/1.1/",
53
+ "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
54
+ "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
51
55
  "manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
56
+ "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
57
+ "number": "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
52
58
  "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
53
59
  "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
54
- "loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
55
60
  "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
56
- "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
57
- "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
58
- "dc": "http://purl.org/dc/elements/1.1/",
61
+ "xlink": "http://www.w3.org/1999/xlink",
59
62
  }
60
63
  for ns, uri in XML_NS.items():
61
64
  ET.register_namespace(ns, uri)
@@ -79,7 +82,6 @@ TAG_SPC = _mkTag("text", "s")
79
82
  TAG_NSPC = _mkTag("text", "c")
80
83
  TAG_TAB = _mkTag("text", "tab")
81
84
  TAG_SPAN = _mkTag("text", "span")
82
- TAG_STNM = _mkTag("text", "style-name")
83
85
 
84
86
  # Formatting Codes
85
87
  X_BLD = 0x001 # Bold format
@@ -89,8 +91,8 @@ X_UND = 0x008 # Underline format
89
91
  X_MRK = 0x010 # Marked format
90
92
  X_SUP = 0x020 # Superscript
91
93
  X_SUB = 0x040 # Subscript
92
- X_DLG = 0x080 # Dialogue
93
- X_DLA = 0x100 # Alt. Dialogue
94
+ X_COL = 0x080 # Coloured text
95
+ X_HRF = 0x100 # Link
94
96
 
95
97
  # Formatting Masks
96
98
  M_BLD = ~X_BLD
@@ -100,8 +102,8 @@ M_UND = ~X_UND
100
102
  M_MRK = ~X_MRK
101
103
  M_SUP = ~X_SUP
102
104
  M_SUB = ~X_SUB
103
- M_DLG = ~X_DLG
104
- M_DLA = ~X_DLA
105
+ M_COL = ~X_COL
106
+ M_HRF = ~X_HRF
105
107
 
106
108
  # ODT Styles
107
109
  S_TITLE = "Title"
@@ -114,6 +116,7 @@ S_FIND = "First_20_line_20_indent"
114
116
  S_TEXT = "Text_20_body"
115
117
  S_META = "Text_20_Meta"
116
118
  S_HNF = "Header_20_and_20_Footer"
119
+ S_NUM = "N0"
117
120
 
118
121
  # Font Data
119
122
  FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
@@ -151,17 +154,15 @@ class ToOdt(Tokenizer):
151
154
 
152
155
  self._mainPara: dict[str, ODTParagraphStyle] = {} # User-accessible paragraph styles
153
156
  self._autoPara: dict[str, ODTParagraphStyle] = {} # Auto-generated paragraph styles
154
- self._autoText: dict[int, ODTTextStyle] = {} # Auto-generated text styles
157
+ self._autoText: dict[str, ODTTextStyle] = {} # Auto-generated text styles
155
158
 
156
- # Footnotes
159
+ # Storage
157
160
  self._nNote = 0
158
- self._etNotes: dict[str, ET.Element] = {} # Generated note elements
159
-
160
161
  self._errData = [] # List of errors encountered
161
162
 
162
163
  # Properties
163
164
  self._textFont = QFont("Liberation Serif", 12)
164
- self._colourHead = False
165
+ self._headWeight = "bold"
165
166
  self._headerFormat = ""
166
167
  self._pageOffset = 0
167
168
 
@@ -183,7 +184,6 @@ class ToOdt(Tokenizer):
183
184
  self._fLineHeight = "115%"
184
185
  self._fBlockIndent = "1.693cm"
185
186
  self._fTextIndent = "0.499cm"
186
- self._textAlign = "left"
187
187
  self._dLanguage = "en"
188
188
  self._dCountry = "GB"
189
189
 
@@ -219,39 +219,14 @@ class ToOdt(Tokenizer):
219
219
  self._mDocLeft = "2.000cm"
220
220
  self._mDocRight = "2.000cm"
221
221
 
222
- # Colour
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"
232
-
233
222
  return
234
223
 
235
224
  ##
236
225
  # Setters
237
226
  ##
238
227
 
239
- def setLanguage(self, language: str | None) -> None:
240
- """Set language for the document."""
241
- if language:
242
- lang, _, country = language.partition("_")
243
- self._dLanguage = lang or self._dLanguage
244
- self._dCountry = country or self._dCountry
245
- return
246
-
247
- def setColourHeaders(self, state: bool) -> None:
248
- """Enable/disable coloured headings and comments."""
249
- self._colourHead = state
250
- return
251
-
252
228
  def setPageLayout(
253
- self, width: int | float, height: int | float,
254
- top: int | float, bottom: int | float, left: int | float, right: int | float
229
+ self, width: float, height: float, top: float, bottom: float, left: float, right: float
255
230
  ) -> None:
256
231
  """Set the document page size and margins in millimetres."""
257
232
  self._mDocWidth = f"{width/10.0:.3f}cm"
@@ -274,6 +249,8 @@ class ToOdt(Tokenizer):
274
249
 
275
250
  def initDocument(self) -> None:
276
251
  """Initialises a new open document XML tree."""
252
+ super().initDocument()
253
+
277
254
  # Initialise Variables
278
255
  # ====================
279
256
 
@@ -281,21 +258,27 @@ class ToOdt(Tokenizer):
281
258
  fontWeight = str(intWeight)
282
259
  fontBold = str(min(intWeight + 300, 900))
283
260
 
261
+ lang, _, country = self._dLocale.name().partition("_")
262
+ self._dLanguage = lang or self._dLanguage
263
+ self._dCountry = country or self._dCountry
264
+
284
265
  self._fontFamily = self._textFont.family()
285
266
  self._fontSize = self._textFont.pointSize()
286
267
  self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight)
287
268
  self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal")
288
269
  self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable"
289
270
  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"
271
+ self._headWeight = self._fontBold if self._boldHeads else None
272
+
273
+ hScale = self._scaleHeads
274
+ self._fSizeTitle = self._emToPt(nwStyles.H_SIZES[0] if hScale else 1.0) # Was 2.50
275
+ self._fSizeHead1 = self._emToPt(nwStyles.H_SIZES[1] if hScale else 1.0) # Was 2.00
276
+ self._fSizeHead2 = self._emToPt(nwStyles.H_SIZES[2] if hScale else 1.0) # Was 1.60
277
+ self._fSizeHead3 = self._emToPt(nwStyles.H_SIZES[3] if hScale else 1.0) # Was 1.30
278
+ self._fSizeHead4 = self._emToPt(nwStyles.H_SIZES[4] if hScale else 1.0) # Was 1.15
279
+ self._fSizeHead = self._emToPt(nwStyles.H_SIZES[4] if hScale else 1.0) # Was 1.15
280
+ self._fSizeText = self._emToPt(1.0)
281
+ self._fSizeFoot = self._emToPt(0.8)
299
282
 
300
283
  mScale = self._lineHeight/1.15
301
284
 
@@ -322,22 +305,9 @@ class ToOdt(Tokenizer):
322
305
  self._mLeftFoot = self._emToCm(self._marginFoot[0])
323
306
  self._mBotFoot = self._emToCm(self._marginFoot[1])
324
307
 
325
- if self._colourHead:
326
- self._colHead12 = "#2a6099"
327
- self._opaHead12 = "100%"
328
- self._colHead34 = "#444444"
329
- self._opaHead34 = "100%"
330
- self._colMetaTx = "#813709"
331
- self._opaMetaTx = "100%"
332
-
333
- if self._showDialog:
334
- self._colDialogM = "#2a6099"
335
- self._colDialogA = "#813709"
336
-
337
308
  self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
338
309
  self._fBlockIndent = self._emToCm(self._blockIndent)
339
310
  self._fTextIndent = self._emToCm(self._firstWidth)
340
- self._textAlign = "justify" if self._doJustify else "left"
341
311
 
342
312
  # Clear Errors
343
313
  self._errData = []
@@ -408,33 +378,21 @@ class ToOdt(Tokenizer):
408
378
  timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
409
379
 
410
380
  # Office Meta Data
411
- xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
412
- xMeta.text = timeStamp
413
-
414
- xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "generator"))
415
- xMeta.text = f"novelWriter/{__version__}"
416
-
417
- xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "initial-creator"))
418
- xMeta.text = self._project.data.author
419
-
420
- xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "editing-cycles"))
421
- xMeta.text = str(self._project.data.saveCount)
381
+ xmlSubElem(self._xMeta, _mkTag("meta", "creation-date"), timeStamp)
382
+ xmlSubElem(self._xMeta, _mkTag("meta", "generator"), f"novelWriter/{__version__}")
383
+ xmlSubElem(self._xMeta, _mkTag("meta", "initial-creator"), self._project.data.author)
384
+ xmlSubElem(self._xMeta, _mkTag("meta", "editing-cycles"), self._project.data.saveCount)
422
385
 
423
386
  # Format is: PnYnMnDTnHnMnS
424
387
  # https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#duration
425
388
  eT = self._project.data.editTime
426
- xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "editing-duration"))
427
- xMeta.text = f"P{eT//86400:d}DT{eT%86400//3600:d}H{eT%3600//60:d}M{eT%60:d}S"
389
+ fT = f"P{eT//86400:d}DT{eT%86400//3600:d}H{eT%3600//60:d}M{eT%60:d}S"
390
+ xmlSubElem(self._xMeta, _mkTag("meta", "editing-duration"), fT)
428
391
 
429
392
  # Dublin Core Meta Data
430
- xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title"))
431
- xMeta.text = self._project.data.name
432
-
433
- xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date"))
434
- xMeta.text = timeStamp
435
-
436
- xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "creator"))
437
- xMeta.text = self._project.data.author
393
+ xmlSubElem(self._xMeta, _mkTag("dc", "title"), self._project.data.name)
394
+ xmlSubElem(self._xMeta, _mkTag("dc", "date"), timeStamp)
395
+ xmlSubElem(self._xMeta, _mkTag("dc", "creator"), self._project.data.author)
438
396
 
439
397
  self._pageStyles()
440
398
  self._defaultStyles()
@@ -445,140 +403,130 @@ class ToOdt(Tokenizer):
445
403
 
446
404
  def doConvert(self) -> None:
447
405
  """Convert the list of text tokens into XML elements."""
448
- self._result = "" # Not used, but cleared just in case
449
-
450
406
  xText = self._xText
451
- for tType, _, tText, tFormat, tStyle in self._tokens:
407
+ for tType, _, tText, tFormat, tStyle in self._blocks:
452
408
 
453
409
  # Styles
454
410
  oStyle = ODTParagraphStyle("New")
455
- if tStyle is not None:
456
- if tStyle & self.A_LEFT:
457
- oStyle.setTextAlign("left")
458
- elif tStyle & self.A_RIGHT:
459
- oStyle.setTextAlign("right")
460
- elif tStyle & self.A_CENTRE:
461
- oStyle.setTextAlign("center")
462
- elif tStyle & self.A_JUSTIFY:
463
- oStyle.setTextAlign("justify")
464
-
465
- if tStyle & self.A_PBB:
466
- oStyle.setBreakBefore("page")
467
- if tStyle & self.A_PBA:
468
- oStyle.setBreakAfter("page")
469
-
470
- if tStyle & self.A_Z_BTMMRG:
471
- oStyle.setMarginBottom("0.000cm")
472
- if tStyle & self.A_Z_TOPMRG:
473
- oStyle.setMarginTop("0.000cm")
474
-
475
- if tStyle & self.A_IND_L:
476
- oStyle.setMarginLeft(self._fBlockIndent)
477
- if tStyle & self.A_IND_R:
478
- oStyle.setMarginRight(self._fBlockIndent)
411
+ if tStyle & BlockFmt.LEFT:
412
+ oStyle.setTextAlign("left")
413
+ elif tStyle & BlockFmt.RIGHT:
414
+ oStyle.setTextAlign("right")
415
+ elif tStyle & BlockFmt.CENTRE:
416
+ oStyle.setTextAlign("center")
417
+ elif tStyle & BlockFmt.JUSTIFY:
418
+ oStyle.setTextAlign("justify")
419
+
420
+ if tStyle & BlockFmt.PBB:
421
+ oStyle.setBreakBefore("page")
422
+ if tStyle & BlockFmt.PBA:
423
+ oStyle.setBreakAfter("page")
424
+
425
+ if tStyle & BlockFmt.Z_BTM:
426
+ oStyle.setMarginBottom("0.000cm")
427
+ if tStyle & BlockFmt.Z_TOP:
428
+ oStyle.setMarginTop("0.000cm")
429
+
430
+ if tStyle & BlockFmt.IND_L:
431
+ oStyle.setMarginLeft(self._fBlockIndent)
432
+ if tStyle & BlockFmt.IND_R:
433
+ oStyle.setMarginRight(self._fBlockIndent)
479
434
 
480
435
  # Process Text Types
481
- if tType == self.T_TEXT:
436
+ if tType == BlockTyp.TEXT:
482
437
  # Text indentation is processed here because there is a
483
438
  # dedicated pre-defined style for it
484
- if tStyle & self.A_IND_T:
439
+ if tStyle & BlockFmt.IND_T:
485
440
  self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
486
441
  else:
487
442
  self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
488
443
 
489
- elif tType == self.T_TITLE:
444
+ elif tType == BlockTyp.TITLE:
490
445
  # Title must be text:p
491
- tHead = tText.replace(nwHeadFmt.BR, "\n")
492
- self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
446
+ self._addTextPar(xText, S_TITLE, oStyle, tText, isHead=False)
493
447
 
494
- elif tType == self.T_HEAD1:
495
- tHead = tText.replace(nwHeadFmt.BR, "\n")
496
- self._addTextPar(xText, S_HEAD1, oStyle, tHead, isHead=True, oLevel="1")
448
+ elif tType == BlockTyp.HEAD1:
449
+ self._addTextPar(xText, S_HEAD1, oStyle, tText, isHead=True, oLevel="1")
497
450
 
498
- elif tType == self.T_HEAD2:
499
- tHead = tText.replace(nwHeadFmt.BR, "\n")
500
- self._addTextPar(xText, S_HEAD2, oStyle, tHead, isHead=True, oLevel="2")
451
+ elif tType == BlockTyp.HEAD2:
452
+ self._addTextPar(xText, S_HEAD2, oStyle, tText, isHead=True, oLevel="2")
501
453
 
502
- elif tType == self.T_HEAD3:
503
- tHead = tText.replace(nwHeadFmt.BR, "\n")
504
- self._addTextPar(xText, S_HEAD3, oStyle, tHead, isHead=True, oLevel="3")
454
+ elif tType == BlockTyp.HEAD3:
455
+ self._addTextPar(xText, S_HEAD3, oStyle, tText, isHead=True, oLevel="3")
505
456
 
506
- elif tType == self.T_HEAD4:
507
- tHead = tText.replace(nwHeadFmt.BR, "\n")
508
- self._addTextPar(xText, S_HEAD4, oStyle, tHead, isHead=True, oLevel="4")
457
+ elif tType == BlockTyp.HEAD4:
458
+ self._addTextPar(xText, S_HEAD4, oStyle, tText, isHead=True, oLevel="4")
509
459
 
510
- elif tType == self.T_SEP:
460
+ elif tType == BlockTyp.SEP:
511
461
  self._addTextPar(xText, S_SEP, oStyle, tText)
512
462
 
513
- elif tType == self.T_SKIP:
463
+ elif tType == BlockTyp.SKIP:
514
464
  self._addTextPar(xText, S_TEXT, oStyle, "")
515
465
 
516
- elif tType == self.T_SYNOPSIS and self._doSynopsis:
517
- tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
518
- self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
519
-
520
- elif tType == self.T_SHORT and self._doSynopsis:
521
- tTemp, tFmt = self._formatSynopsis(tText, tFormat, False)
522
- self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
466
+ elif tType == BlockTyp.COMMENT:
467
+ self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat)
523
468
 
524
- elif tType == self.T_COMMENT and self._doComments:
525
- tTemp, tFmt = self._formatComments(tText, tFormat)
526
- self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
527
-
528
- elif tType == self.T_KEYWORD and self._doKeywords:
529
- tTemp, tFmt = self._formatKeywords(tText)
530
- self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
469
+ elif tType == BlockTyp.KEYWORD:
470
+ self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat)
531
471
 
532
472
  return
533
473
 
534
474
  def closeDocument(self) -> None:
535
- """Pack the automatic styles of the XML document."""
475
+ """Add additional collected information to the XML."""
536
476
  for style in self._autoPara.values():
537
477
  style.packXML(self._xAuto)
538
478
  for style in self._autoText.values():
539
479
  style.packXML(self._xAuto)
480
+ if self._counts:
481
+ xFields = ET.Element(_mkTag("text", "user-field-decls"))
482
+ for key, value in self._counts.items():
483
+ ET.SubElement(xFields, _mkTag("text", "user-field-decl"), attrib={
484
+ _mkTag("office", "value-type"): "float",
485
+ _mkTag("office", "value"): str(value),
486
+ _mkTag("text", "name"): f"Manuscript{key[:1].upper()}{key[1:]}",
487
+ })
488
+ self._xText.insert(0, xFields)
540
489
  return
541
490
 
542
- def saveFlatXML(self, path: str | Path) -> None:
543
- """Save the data to an .fodt file."""
544
- with open(path, mode="wb") as fObj:
545
- xml = ET.ElementTree(self._dFlat)
546
- xmlIndent(xml)
547
- xml.write(fObj, encoding="utf-8", xml_declaration=True)
548
- logger.info("Wrote file: %s", path)
549
- return
550
-
551
- def saveOpenDocText(self, path: str | Path) -> None:
552
- """Save the data to an .odt file."""
553
- mMani = _mkTag("manifest", "manifest")
554
- mVers = _mkTag("manifest", "version")
555
- mPath = _mkTag("manifest", "full-path")
556
- mType = _mkTag("manifest", "media-type")
557
- mFile = _mkTag("manifest", "file-entry")
558
-
559
- xMani = ET.Element(mMani, attrib={mVers: X_VERS})
560
- ET.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
561
- ET.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
562
- ET.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
563
- ET.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
564
- ET.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
565
-
566
- oRoot = _mkTag("office", "document-settings")
567
- oVers = _mkTag("office", "version")
568
- xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
569
-
570
- def putInZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
571
- with zipObj.open(name, mode="w") as fObj:
572
- xml = ET.ElementTree(xObj)
491
+ def saveDocument(self, path: Path) -> None:
492
+ """Save the data to an .fodt or .odt file."""
493
+ if self._isFlat:
494
+ with open(path, mode="wb") as fObj:
495
+ xml = ET.ElementTree(self._dFlat)
496
+ xmlIndent(xml)
573
497
  xml.write(fObj, encoding="utf-8", xml_declaration=True)
574
498
 
575
- with ZipFile(path, mode="w") as outZip:
576
- outZip.writestr("mimetype", X_MIME)
577
- putInZip("META-INF/manifest.xml", xMani, outZip)
578
- putInZip("settings.xml", xSett, outZip)
579
- putInZip("content.xml", self._dCont, outZip)
580
- putInZip("meta.xml", self._dMeta, outZip)
581
- putInZip("styles.xml", self._dStyl, outZip)
499
+ else:
500
+ mMani = _mkTag("manifest", "manifest")
501
+ mVers = _mkTag("manifest", "version")
502
+ mPath = _mkTag("manifest", "full-path")
503
+ mType = _mkTag("manifest", "media-type")
504
+ mFile = _mkTag("manifest", "file-entry")
505
+
506
+ xMani = ET.Element(mMani, attrib={mVers: X_VERS})
507
+ ET.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
508
+ ET.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
509
+ ET.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
510
+ ET.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
511
+ ET.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
512
+
513
+ oRoot = _mkTag("office", "document-settings")
514
+ oVers = _mkTag("office", "version")
515
+ xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
516
+
517
+ def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
518
+ zipObj.writestr(
519
+ name, ET.tostring(root, encoding="utf-8", xml_declaration=True),
520
+ compress_type=ZIP_DEFLATED, compresslevel=3,
521
+ )
522
+
523
+ with ZipFile(path, mode="w") as outZip:
524
+ outZip.writestr("mimetype", X_MIME, compress_type=None, compresslevel=None)
525
+ xmlToZip("META-INF/manifest.xml", xMani, outZip)
526
+ xmlToZip("settings.xml", xSett, outZip)
527
+ xmlToZip("content.xml", self._dCont, outZip)
528
+ xmlToZip("meta.xml", self._dMeta, outZip)
529
+ xmlToZip("styles.xml", self._dStyl, outZip)
582
530
 
583
531
  logger.info("Wrote file: %s", path)
584
532
 
@@ -588,43 +536,14 @@ class ToOdt(Tokenizer):
588
536
  # Internal Functions
589
537
  ##
590
538
 
591
- def _formatSynopsis(self, text: str, fmt: T_Formats, synopsis: bool) -> tuple[str, T_Formats]:
592
- """Apply formatting to synopsis lines."""
593
- name = self._localLookup("Synopsis" if synopsis else "Short Description")
594
- shift = len(name) + 2
595
- rTxt = f"{name}: {text}"
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)
598
- return rTxt, rFmt
599
-
600
- def _formatComments(self, text: str, fmt: T_Formats) -> tuple[str, T_Formats]:
601
- """Apply formatting to comments."""
602
- name = self._localLookup("Comment")
603
- shift = len(name) + 2
604
- rTxt = f"{name}: {text}"
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)
607
- return rTxt, rFmt
608
-
609
- def _formatKeywords(self, text: str) -> tuple[str, T_Formats]:
610
- """Apply formatting to keywords."""
611
- valid, bits, _ = self._project.index.scanThis("@"+text)
612
- if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
613
- return "", []
614
-
615
- rTxt = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
616
- rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(rTxt) - 1, self.FMT_B_E, "")]
617
- if len(bits) > 1:
618
- if bits[0] == nwKeyWords.TAG_KEY:
619
- rTxt += bits[1]
620
- else:
621
- rTxt += ", ".join(bits[1:])
622
-
623
- return rTxt, rFmt
624
-
625
539
  def _addTextPar(
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
540
+ self,
541
+ xParent: ET.Element,
542
+ styleName: str, oStyle: ODTParagraphStyle,
543
+ tText: str,
544
+ tFmt: Sequence[tuple[int, int, str]] | None = None,
545
+ isHead: bool = False,
546
+ oLevel: str | None = None,
628
547
  ) -> None:
629
548
  """Add a text paragraph to the text XML element."""
630
549
  tAttr = {_mkTag("text", "style-name"): self._paraStyle(styleName, oStyle)}
@@ -646,12 +565,13 @@ class ToOdt(Tokenizer):
646
565
 
647
566
  parProc = XMLParagraph(xElem)
648
567
 
649
- pErr = 0
650
568
  xFmt = 0x00
651
569
  tFrag = ""
652
570
  fLast = 0
653
571
  xNode = None
654
- for fPos, fFmt, fData in tFmt:
572
+ fClass = ""
573
+ fLink = ""
574
+ for fPos, fFmt, fData in tFmt or []:
655
575
 
656
576
  # Add any extra nodes
657
577
  if xNode is not None:
@@ -663,51 +583,55 @@ class ToOdt(Tokenizer):
663
583
  if xFmt == 0x00:
664
584
  parProc.appendText(tFrag)
665
585
  else:
666
- parProc.appendSpan(tFrag, self._textStyle(xFmt))
586
+ parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass), fLink)
667
587
 
668
588
  # Calculate the change of format
669
- if fFmt == self.FMT_B_B:
589
+ if fFmt == TextFmt.B_B:
670
590
  xFmt |= X_BLD
671
- elif fFmt == self.FMT_B_E:
591
+ elif fFmt == TextFmt.B_E:
672
592
  xFmt &= M_BLD
673
- elif fFmt == self.FMT_I_B:
593
+ elif fFmt == TextFmt.I_B:
674
594
  xFmt |= X_ITA
675
- elif fFmt == self.FMT_I_E:
595
+ elif fFmt == TextFmt.I_E:
676
596
  xFmt &= M_ITA
677
- elif fFmt == self.FMT_D_B:
597
+ elif fFmt == TextFmt.D_B:
678
598
  xFmt |= X_DEL
679
- elif fFmt == self.FMT_D_E:
599
+ elif fFmt == TextFmt.D_E:
680
600
  xFmt &= M_DEL
681
- elif fFmt == self.FMT_U_B:
601
+ elif fFmt == TextFmt.U_B:
682
602
  xFmt |= X_UND
683
- elif fFmt == self.FMT_U_E:
603
+ elif fFmt == TextFmt.U_E:
684
604
  xFmt &= M_UND
685
- elif fFmt == self.FMT_M_B:
605
+ elif fFmt == TextFmt.M_B:
686
606
  xFmt |= X_MRK
687
- elif fFmt == self.FMT_M_E:
607
+ elif fFmt == TextFmt.M_E:
688
608
  xFmt &= M_MRK
689
- elif fFmt == self.FMT_SUP_B:
609
+ elif fFmt == TextFmt.SUP_B:
690
610
  xFmt |= X_SUP
691
- elif fFmt == self.FMT_SUP_E:
611
+ elif fFmt == TextFmt.SUP_E:
692
612
  xFmt &= M_SUP
693
- elif fFmt == self.FMT_SUB_B:
613
+ elif fFmt == TextFmt.SUB_B:
694
614
  xFmt |= X_SUB
695
- elif fFmt == self.FMT_SUB_E:
615
+ elif fFmt == TextFmt.SUB_E:
696
616
  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:
617
+ elif fFmt == TextFmt.COL_B:
618
+ xFmt |= X_COL
619
+ fClass = fData
620
+ elif fFmt == TextFmt.COL_E:
621
+ xFmt &= M_COL
622
+ fClass = ""
623
+ elif fFmt == TextFmt.HRF_B:
624
+ xFmt |= X_HRF
625
+ fLink = fData
626
+ elif fFmt == TextFmt.HRF_E:
627
+ xFmt &= M_HRF
628
+ fLink = ""
629
+ elif fFmt == TextFmt.FNOTE:
706
630
  xNode = self._generateFootnote(fData)
707
- elif fFmt == self.FMT_STRIP:
631
+ elif fFmt == TextFmt.FIELD:
632
+ xNode = self._generateField(fData, xFmt)
633
+ elif fFmt == TextFmt.STRIP:
708
634
  pass
709
- else:
710
- pErr += 1
711
635
 
712
636
  fLast = fPos
713
637
 
@@ -718,10 +642,7 @@ class ToOdt(Tokenizer):
718
642
  if xFmt == 0x00:
719
643
  parProc.appendText(tFrag)
720
644
  else:
721
- parProc.appendSpan(tFrag, self._textStyle(xFmt))
722
-
723
- if pErr > 0:
724
- self._errData.append("Unknown format tag encountered")
645
+ parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass), fLink)
725
646
 
726
647
  nErr, errMsg = parProc.checkError()
727
648
  if nErr > 0: # pragma: no cover
@@ -752,10 +673,13 @@ class ToOdt(Tokenizer):
752
673
 
753
674
  return modStyle.name
754
675
 
755
- def _textStyle(self, hFmt: int) -> str:
676
+ def _textStyle(self, hFmt: int, fClass: str = "") -> str:
756
677
  """Return a text style for a given style code."""
757
- if hFmt in self._autoText:
758
- return self._autoText[hFmt].name
678
+ tKey = str(hFmt)
679
+ if fClass and (color := self._classes.get(fClass)):
680
+ tKey = f"{tKey}:{fClass}"
681
+ if tKey in self._autoText:
682
+ return self._autoText[tKey].name
759
683
 
760
684
  style = ODTTextStyle(f"T{len(self._autoText)+1:d}")
761
685
  if hFmt & X_BLD:
@@ -768,18 +692,21 @@ class ToOdt(Tokenizer):
768
692
  if hFmt & X_UND:
769
693
  style.setUnderlineStyle("solid")
770
694
  style.setUnderlineWidth("auto")
771
- style.setUnderlineColour("font-color")
695
+ style.setUnderlineColor("font-color")
772
696
  if hFmt & X_MRK:
773
- style.setBackgroundColour(self._markText)
697
+ style.setBackgroundColor(self._theme.highlight)
774
698
  if hFmt & X_SUP:
775
699
  style.setTextPosition("super")
776
700
  if hFmt & X_SUB:
777
701
  style.setTextPosition("sub")
778
- if hFmt & X_DLG:
779
- style.setColour(self._colDialogM)
780
- if hFmt & X_DLA:
781
- style.setColour(self._colDialogA)
782
- self._autoText[hFmt] = style
702
+ if hFmt & X_COL and color:
703
+ style.setColor(color)
704
+ if hFmt & X_HRF:
705
+ style.setColor(self._theme.link)
706
+ style.setUnderlineStyle("solid")
707
+ style.setUnderlineWidth("auto")
708
+ style.setUnderlineColor("font-color")
709
+ self._autoText[tKey] = style
783
710
 
784
711
  return style.name
785
712
 
@@ -788,21 +715,41 @@ class ToOdt(Tokenizer):
788
715
  if content := self._footnotes.get(key):
789
716
  self._nNote += 1
790
717
  nStyle = ODTParagraphStyle("New")
791
- xNote = ET.Element(_mkTag("text", "note"), attrib={
718
+ xNote = xmlElement(_mkTag("text", "note"), attrib={
792
719
  _mkTag("text", "id"): f"ftn{self._nNote}",
793
720
  _mkTag("text", "note-class"): "footnote",
794
721
  })
795
- xCite = ET.SubElement(xNote, _mkTag("text", "note-citation"))
796
- xCite.text = str(self._nNote)
797
- xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
722
+ xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote)
723
+ xBody = xmlSubElem(xNote, _mkTag("text", "note-body"))
798
724
  self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
799
725
  return xNote
800
726
  return None
801
727
 
728
+ def _generateField(self, key: str, fmt: int) -> ET.Element | None:
729
+ """Generate a data field XML object."""
730
+ if key and (field := key.partition(":")[2]):
731
+ xField = xmlElement(_mkTag("text", "user-field-get"), "0", tail="", attrib={
732
+ _mkTag("style", "data-style-name"): S_NUM,
733
+ _mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}",
734
+ })
735
+ if fmt == 0x00:
736
+ return xField
737
+ else:
738
+ xSpan = xmlElement(TAG_SPAN, "", tail="", attrib={
739
+ _mkTag("text", "style-name"): self._textStyle(fmt),
740
+ })
741
+ xSpan.append(xField)
742
+ return xSpan
743
+ return None
744
+
802
745
  def _emToCm(self, value: float) -> str:
803
746
  """Converts an em value to centimetres."""
804
747
  return f"{value*2.54/72*self._fontSize:.3f}cm"
805
748
 
749
+ def _emToPt(self, scale: float) -> str:
750
+ """Compute relative font size in points."""
751
+ return f"{round(scale * self._fontSize):d}pt"
752
+
806
753
  ##
807
754
  # Style Elements
808
755
  ##
@@ -898,10 +845,20 @@ class ToOdt(Tokenizer):
898
845
  _mkTag("style", "class"): "extra",
899
846
  })
900
847
 
848
+ # Numbers Style
849
+ xStyl = ET.SubElement(self._xStyl, _mkTag("number", "number-style"), attrib={
850
+ _mkTag("style", "name"): S_NUM,
851
+ })
852
+ ET.SubElement(xStyl, _mkTag("number", "number"), attrib={
853
+ _mkTag("number", "min-integer-digits"): "1",
854
+ })
855
+
901
856
  return
902
857
 
903
858
  def _useableStyles(self) -> None:
904
859
  """Set the usable styles."""
860
+ hColor = self._theme.head if self._colorHeads else None
861
+
905
862
  # Add Text Body Style
906
863
  style = ODTParagraphStyle(S_TEXT)
907
864
  style.setDisplayName("Text body")
@@ -910,7 +867,7 @@ class ToOdt(Tokenizer):
910
867
  style.setMarginTop(self._mTopText)
911
868
  style.setMarginBottom(self._mBotText)
912
869
  style.setLineHeight(self._fLineHeight)
913
- style.setTextAlign(self._textAlign)
870
+ style.setTextAlign(self._defaultAlign)
914
871
  style.setFontName(self._fontFamily)
915
872
  style.setFontFamily(self._fontFamily)
916
873
  style.setFontSize(self._fSizeText)
@@ -939,8 +896,6 @@ class ToOdt(Tokenizer):
939
896
  style.setFontFamily(self._fontFamily)
940
897
  style.setFontSize(self._fSizeText)
941
898
  style.setFontWeight(self._fontWeight)
942
- style.setColour(self._colMetaTx)
943
- style.setOpacity(self._opaMetaTx)
944
899
  style.packXML(self._xStyl)
945
900
  self._mainPara[style.name] = style
946
901
 
@@ -956,7 +911,7 @@ class ToOdt(Tokenizer):
956
911
  style.setFontName(self._fontFamily)
957
912
  style.setFontFamily(self._fontFamily)
958
913
  style.setFontSize(self._fSizeTitle)
959
- style.setFontWeight(self._fontBold)
914
+ style.setFontWeight(self._headWeight)
960
915
  style.packXML(self._xStyl)
961
916
  self._mainPara[style.name] = style
962
917
 
@@ -989,9 +944,8 @@ class ToOdt(Tokenizer):
989
944
  style.setFontName(self._fontFamily)
990
945
  style.setFontFamily(self._fontFamily)
991
946
  style.setFontSize(self._fSizeHead1)
992
- style.setFontWeight(self._fontBold)
993
- style.setColour(self._colHead12)
994
- style.setOpacity(self._opaHead12)
947
+ style.setFontWeight(self._headWeight)
948
+ style.setColor(hColor)
995
949
  style.packXML(self._xStyl)
996
950
  self._mainPara[style.name] = style
997
951
 
@@ -1007,9 +961,8 @@ class ToOdt(Tokenizer):
1007
961
  style.setFontName(self._fontFamily)
1008
962
  style.setFontFamily(self._fontFamily)
1009
963
  style.setFontSize(self._fSizeHead2)
1010
- style.setFontWeight(self._fontBold)
1011
- style.setColour(self._colHead12)
1012
- style.setOpacity(self._opaHead12)
964
+ style.setFontWeight(self._headWeight)
965
+ style.setColor(hColor)
1013
966
  style.packXML(self._xStyl)
1014
967
  self._mainPara[style.name] = style
1015
968
 
@@ -1025,9 +978,8 @@ class ToOdt(Tokenizer):
1025
978
  style.setFontName(self._fontFamily)
1026
979
  style.setFontFamily(self._fontFamily)
1027
980
  style.setFontSize(self._fSizeHead3)
1028
- style.setFontWeight(self._fontBold)
1029
- style.setColour(self._colHead34)
1030
- style.setOpacity(self._opaHead34)
981
+ style.setFontWeight(self._headWeight)
982
+ style.setColor(hColor)
1031
983
  style.packXML(self._xStyl)
1032
984
  self._mainPara[style.name] = style
1033
985
 
@@ -1043,9 +995,8 @@ class ToOdt(Tokenizer):
1043
995
  style.setFontName(self._fontFamily)
1044
996
  style.setFontFamily(self._fontFamily)
1045
997
  style.setFontSize(self._fSizeHead4)
1046
- style.setFontWeight(self._fontBold)
1047
- style.setColour(self._colHead34)
1048
- style.setOpacity(self._opaHead34)
998
+ style.setFontWeight(self._headWeight)
999
+ style.setColor(hColor)
1049
1000
  style.packXML(self._xStyl)
1050
1001
  self._mainPara[style.name] = style
1051
1002
 
@@ -1080,12 +1031,12 @@ class ToOdt(Tokenizer):
1080
1031
 
1081
1032
  # Standard Page Header
1082
1033
  if self._headerFormat:
1083
- pre, page, post = self._headerFormat.partition(nwHeadFmt.ODT_PAGE)
1034
+ pre, page, post = self._headerFormat.partition(nwHeadFmt.DOC_PAGE)
1084
1035
 
1085
- pre = pre.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name)
1086
- pre = pre.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author)
1087
- post = post.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name)
1088
- post = post.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author)
1036
+ pre = pre.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
1037
+ pre = pre.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
1038
+ post = post.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
1039
+ post = post.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
1089
1040
 
1090
1041
  xHead = ET.SubElement(xPage, _mkTag("style", "header"))
1091
1042
  xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
@@ -1167,16 +1118,6 @@ class ODTParagraphStyle:
1167
1118
  def name(self) -> str:
1168
1119
  return self._name
1169
1120
 
1170
- ##
1171
- # Checkers
1172
- ##
1173
-
1174
- def isUnaligned(self) -> bool:
1175
- """Check if paragraph has any sort of alignment or margins."""
1176
- return all(
1177
- self._pAttr[n][1] is None for n in ["text-align", "margin-left", "margin-right"]
1178
- )
1179
-
1180
1121
  ##
1181
1122
  # Setters
1182
1123
  ##
@@ -1306,14 +1247,14 @@ class ODTParagraphStyle:
1306
1247
  self._tAttr["font-weight"][1] = None
1307
1248
  return
1308
1249
 
1309
- def setColour(self, value: str | None) -> None:
1250
+ def setColor(self, value: QColor | None) -> None:
1310
1251
  """Set text colour."""
1311
- self._tAttr["color"][1] = value
1312
- return
1313
-
1314
- def setOpacity(self, value: str | None) -> None:
1315
- """Set text opacity."""
1316
- self._tAttr["opacity"][1] = value
1252
+ if isinstance(value, QColor):
1253
+ self._tAttr["color"][1] = value.name(QtHexRgb)
1254
+ self._tAttr["opacity"][1] = f"{int(100.0 * value.alphaF())}%"
1255
+ else:
1256
+ self._tAttr["color"][1] = None
1257
+ self._tAttr["opacity"][1] = None
1317
1258
  return
1318
1259
 
1319
1260
  ##
@@ -1414,18 +1355,18 @@ class ODTTextStyle:
1414
1355
  self._tAttr["font-style"][1] = None
1415
1356
  return
1416
1357
 
1417
- def setColour(self, value: str | None) -> None:
1358
+ def setColor(self, value: QColor | None) -> None:
1418
1359
  """Set text colour."""
1419
- if value and len(value) == 7 and value[0] == "#":
1420
- self._tAttr["color"][1] = value
1360
+ if isinstance(value, QColor):
1361
+ self._tAttr["color"][1] = value.name(QtHexRgb)
1421
1362
  else:
1422
1363
  self._tAttr["color"][1] = None
1423
1364
  return
1424
1365
 
1425
- def setBackgroundColour(self, value: str | None) -> None:
1366
+ def setBackgroundColor(self, value: QColor | None) -> None:
1426
1367
  """Set text background colour."""
1427
- if value and len(value) == 7 and value[0] == "#":
1428
- self._tAttr["background-color"][1] = value
1368
+ if isinstance(value, QColor):
1369
+ self._tAttr["background-color"][1] = value.name(QtHexRgb)
1429
1370
  else:
1430
1371
  self._tAttr["background-color"][1] = None
1431
1372
  return
@@ -1470,7 +1411,7 @@ class ODTTextStyle:
1470
1411
  self._tAttr["text-underline-width"][1] = None
1471
1412
  return
1472
1413
 
1473
- def setUnderlineColour(self, value: str | None) -> None:
1414
+ def setUnderlineColor(self, value: str | None) -> None:
1474
1415
  """Set text underline colour."""
1475
1416
  if value in self.VALID_LCOL:
1476
1417
  self._tAttr["text-underline-color"][1] = value
@@ -1595,13 +1536,22 @@ class XMLParagraph:
1595
1536
 
1596
1537
  return
1597
1538
 
1598
- def appendSpan(self, text: str, fmt: str) -> None:
1539
+ def appendSpan(self, text: str, style: str, link: str) -> None:
1599
1540
  """Append a text span to the XML element. The span is always
1600
- closed since we do not allow nested spans (like Libre Office).
1541
+ closed since we do not produce nested spans (like Libre Office).
1601
1542
  Therefore we return to the root element level when we're done
1602
1543
  processing the text of the span.
1603
1544
  """
1604
- self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: fmt})
1545
+ if link:
1546
+ self._xTail = ET.SubElement(self._xRoot, _mkTag("text", "a"), attrib={
1547
+ _mkTag("xlink", "type"): "simple",
1548
+ _mkTag("xlink", "href"): link,
1549
+ _mkTag("text", "style-name"): style,
1550
+ })
1551
+ else:
1552
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={
1553
+ _mkTag("text", "style-name"): style,
1554
+ })
1605
1555
  self._xTail.text = "" # Defaults to None
1606
1556
  self._xTail.tail = "" # Defaults to None
1607
1557
  self._nState = X_SPAN_TEXT