novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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 (83) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
  3. novelwriter/__init__.py +49 -10
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  6. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  8. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  9. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  10. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  14. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  17. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  18. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  23. novelwriter/assets/manual.pdf +0 -0
  24. novelwriter/assets/sample.zip +0 -0
  25. novelwriter/common.py +100 -2
  26. novelwriter/config.py +25 -15
  27. novelwriter/constants.py +168 -60
  28. novelwriter/core/buildsettings.py +66 -39
  29. novelwriter/core/coretools.py +145 -147
  30. novelwriter/core/docbuild.py +132 -170
  31. novelwriter/core/index.py +38 -37
  32. novelwriter/core/item.py +41 -8
  33. novelwriter/core/itemmodel.py +518 -0
  34. novelwriter/core/options.py +4 -1
  35. novelwriter/core/project.py +67 -89
  36. novelwriter/core/spellcheck.py +9 -14
  37. novelwriter/core/status.py +7 -5
  38. novelwriter/core/tree.py +268 -287
  39. novelwriter/dialogs/docmerge.py +7 -17
  40. novelwriter/dialogs/preferences.py +46 -33
  41. novelwriter/dialogs/projectsettings.py +5 -5
  42. novelwriter/enum.py +36 -23
  43. novelwriter/extensions/configlayout.py +27 -12
  44. novelwriter/extensions/modified.py +13 -1
  45. novelwriter/extensions/pagedsidebar.py +5 -5
  46. novelwriter/formats/shared.py +155 -0
  47. novelwriter/formats/todocx.py +1191 -0
  48. novelwriter/formats/tohtml.py +451 -0
  49. novelwriter/{core → formats}/tokenizer.py +487 -491
  50. novelwriter/formats/tomarkdown.py +217 -0
  51. novelwriter/{core → formats}/toodt.py +311 -432
  52. novelwriter/formats/toqdoc.py +484 -0
  53. novelwriter/formats/toraw.py +91 -0
  54. novelwriter/gui/doceditor.py +342 -284
  55. novelwriter/gui/dochighlight.py +96 -84
  56. novelwriter/gui/docviewer.py +88 -31
  57. novelwriter/gui/docviewerpanel.py +17 -25
  58. novelwriter/gui/editordocument.py +17 -2
  59. novelwriter/gui/itemdetails.py +25 -28
  60. novelwriter/gui/mainmenu.py +129 -63
  61. novelwriter/gui/noveltree.py +45 -47
  62. novelwriter/gui/outline.py +196 -249
  63. novelwriter/gui/projtree.py +594 -1241
  64. novelwriter/gui/search.py +9 -10
  65. novelwriter/gui/sidebar.py +7 -6
  66. novelwriter/gui/theme.py +10 -5
  67. novelwriter/guimain.py +100 -196
  68. novelwriter/shared.py +66 -27
  69. novelwriter/text/counting.py +2 -0
  70. novelwriter/text/patterns.py +168 -60
  71. novelwriter/tools/manusbuild.py +14 -12
  72. novelwriter/tools/manuscript.py +120 -78
  73. novelwriter/tools/manussettings.py +424 -291
  74. novelwriter/tools/welcome.py +4 -4
  75. novelwriter/tools/writingstats.py +3 -3
  76. novelwriter/types.py +23 -7
  77. novelwriter/core/tohtml.py +0 -530
  78. novelwriter/core/tomarkdown.py +0 -252
  79. novelwriter/core/toqdoc.py +0 -419
  80. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  81. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
  82. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  83. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.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, 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
 
@@ -172,45 +173,10 @@ class ToOdt(Tokenizer):
172
173
  self._fontStyle = "normal"
173
174
  self._fontPitch = "variable"
174
175
  self._fontBold = "bold"
175
- self._fSizeTitle = "30pt"
176
- self._fSizeHead1 = "24pt"
177
- self._fSizeHead2 = "20pt"
178
- self._fSizeHead3 = "16pt"
179
- self._fSizeHead4 = "14pt"
180
- self._fSizeHead = "14pt"
181
- self._fSizeText = "12pt"
182
- self._fSizeFoot = "10pt"
183
- self._fLineHeight = "115%"
184
176
  self._fBlockIndent = "1.693cm"
185
- self._fTextIndent = "0.499cm"
186
- self._textAlign = "left"
187
177
  self._dLanguage = "en"
188
178
  self._dCountry = "GB"
189
179
 
190
- # Text Margins
191
- self._mTopTitle = "0.423cm"
192
- self._mTopHead1 = "0.423cm"
193
- self._mTopHead2 = "0.353cm"
194
- self._mTopHead3 = "0.247cm"
195
- self._mTopHead4 = "0.247cm"
196
- self._mTopHead = "0.423cm"
197
- self._mTopText = "0.000cm"
198
- self._mTopMeta = "0.000cm"
199
- self._mTopSep = "0.247cm"
200
-
201
- self._mBotTitle = "0.212cm"
202
- self._mBotHead1 = "0.212cm"
203
- self._mBotHead2 = "0.212cm"
204
- self._mBotHead3 = "0.212cm"
205
- self._mBotHead4 = "0.212cm"
206
- self._mBotHead = "0.212cm"
207
- self._mBotText = "0.247cm"
208
- self._mBotMeta = "0.106cm"
209
- self._mBotSep = "0.247cm"
210
-
211
- self._mBotFoot = "0.106cm"
212
- self._mLeftFoot = "0.600cm"
213
-
214
180
  # Document Size and Margins
215
181
  self._mDocWidth = "21.0cm"
216
182
  self._mDocHeight = "29.7cm"
@@ -219,39 +185,14 @@ class ToOdt(Tokenizer):
219
185
  self._mDocLeft = "2.000cm"
220
186
  self._mDocRight = "2.000cm"
221
187
 
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
188
  return
234
189
 
235
190
  ##
236
191
  # Setters
237
192
  ##
238
193
 
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
194
  def setPageLayout(
253
- self, width: int | float, height: int | float,
254
- top: int | float, bottom: int | float, left: int | float, right: int | float
195
+ self, width: float, height: float, top: float, bottom: float, left: float, right: float
255
196
  ) -> None:
256
197
  """Set the document page size and margins in millimetres."""
257
198
  self._mDocWidth = f"{width/10.0:.3f}cm"
@@ -274,70 +215,21 @@ class ToOdt(Tokenizer):
274
215
 
275
216
  def initDocument(self) -> None:
276
217
  """Initialises a new open document XML tree."""
218
+ super().initDocument()
219
+
277
220
  # Initialise Variables
278
221
  # ====================
279
222
 
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"
299
-
300
- mScale = self._lineHeight/1.15
301
-
302
- self._mTopTitle = self._emToCm(mScale * self._marginTitle[0])
303
- self._mTopHead1 = self._emToCm(mScale * self._marginHead1[0])
304
- self._mTopHead2 = self._emToCm(mScale * self._marginHead2[0])
305
- self._mTopHead3 = self._emToCm(mScale * self._marginHead3[0])
306
- self._mTopHead4 = self._emToCm(mScale * self._marginHead4[0])
307
- self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
308
- self._mTopText = self._emToCm(mScale * self._marginText[0])
309
- self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
310
- self._mTopSep = self._emToCm(mScale * self._marginSep[0])
311
-
312
- self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
313
- self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
314
- self._mBotHead2 = self._emToCm(mScale * self._marginHead2[1])
315
- self._mBotHead3 = self._emToCm(mScale * self._marginHead3[1])
316
- self._mBotHead4 = self._emToCm(mScale * self._marginHead4[1])
317
- self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
318
- self._mBotText = self._emToCm(mScale * self._marginText[1])
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])
324
-
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
- self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
223
+ lang, _, country = self._dLocale.name().partition("_")
224
+ self._dLanguage = lang or self._dLanguage
225
+ self._dCountry = country or self._dCountry
226
+
227
+ self._fontFamily = self._textFont.family()
228
+ self._fontSize = self._textFont.pointSize()
229
+ self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal")
230
+ self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable"
231
+ self._headWeight = self._fontBold if self._boldHeads else None
338
232
  self._fBlockIndent = self._emToCm(self._blockIndent)
339
- self._fTextIndent = self._emToCm(self._firstWidth)
340
- self._textAlign = "justify" if self._doJustify else "left"
341
233
 
342
234
  # Clear Errors
343
235
  self._errData = []
@@ -408,33 +300,21 @@ class ToOdt(Tokenizer):
408
300
  timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
409
301
 
410
302
  # 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)
303
+ xmlSubElem(self._xMeta, _mkTag("meta", "creation-date"), timeStamp)
304
+ xmlSubElem(self._xMeta, _mkTag("meta", "generator"), f"novelWriter/{__version__}")
305
+ xmlSubElem(self._xMeta, _mkTag("meta", "initial-creator"), self._project.data.author)
306
+ xmlSubElem(self._xMeta, _mkTag("meta", "editing-cycles"), self._project.data.saveCount)
422
307
 
423
308
  # Format is: PnYnMnDTnHnMnS
424
309
  # https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#duration
425
310
  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"
311
+ fT = f"P{eT//86400:d}DT{eT%86400//3600:d}H{eT%3600//60:d}M{eT%60:d}S"
312
+ xmlSubElem(self._xMeta, _mkTag("meta", "editing-duration"), fT)
428
313
 
429
314
  # 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
315
+ xmlSubElem(self._xMeta, _mkTag("dc", "title"), self._project.data.name)
316
+ xmlSubElem(self._xMeta, _mkTag("dc", "date"), timeStamp)
317
+ xmlSubElem(self._xMeta, _mkTag("dc", "creator"), self._project.data.author)
438
318
 
439
319
  self._pageStyles()
440
320
  self._defaultStyles()
@@ -445,140 +325,130 @@ class ToOdt(Tokenizer):
445
325
 
446
326
  def doConvert(self) -> None:
447
327
  """Convert the list of text tokens into XML elements."""
448
- self._result = "" # Not used, but cleared just in case
449
-
450
328
  xText = self._xText
451
- for tType, _, tText, tFormat, tStyle in self._tokens:
329
+ for tType, _, tText, tFormat, tStyle in self._blocks:
452
330
 
453
331
  # Styles
454
332
  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)
333
+ if tStyle & BlockFmt.LEFT:
334
+ oStyle.setTextAlign("left")
335
+ elif tStyle & BlockFmt.RIGHT:
336
+ oStyle.setTextAlign("right")
337
+ elif tStyle & BlockFmt.CENTRE:
338
+ oStyle.setTextAlign("center")
339
+ elif tStyle & BlockFmt.JUSTIFY:
340
+ oStyle.setTextAlign("justify")
341
+
342
+ if tStyle & BlockFmt.PBB:
343
+ oStyle.setBreakBefore("page")
344
+ if tStyle & BlockFmt.PBA:
345
+ oStyle.setBreakAfter("page")
346
+
347
+ if tStyle & BlockFmt.Z_BTM:
348
+ oStyle.setMarginBottom("0.000cm")
349
+ if tStyle & BlockFmt.Z_TOP:
350
+ oStyle.setMarginTop("0.000cm")
351
+
352
+ if tStyle & BlockFmt.IND_L:
353
+ oStyle.setMarginLeft(self._fBlockIndent)
354
+ if tStyle & BlockFmt.IND_R:
355
+ oStyle.setMarginRight(self._fBlockIndent)
479
356
 
480
357
  # Process Text Types
481
- if tType == self.T_TEXT:
358
+ if tType == BlockTyp.TEXT:
482
359
  # Text indentation is processed here because there is a
483
360
  # dedicated pre-defined style for it
484
- if tStyle & self.A_IND_T:
361
+ if tStyle & BlockFmt.IND_T:
485
362
  self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
486
363
  else:
487
364
  self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
488
365
 
489
- elif tType == self.T_TITLE:
366
+ elif tType == BlockTyp.TITLE:
490
367
  # Title must be text:p
491
- tHead = tText.replace(nwHeadFmt.BR, "\n")
492
- self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
368
+ self._addTextPar(xText, S_TITLE, oStyle, tText, isHead=False)
493
369
 
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")
370
+ elif tType == BlockTyp.HEAD1:
371
+ self._addTextPar(xText, S_HEAD1, oStyle, tText, isHead=True, oLevel="1")
497
372
 
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")
373
+ elif tType == BlockTyp.HEAD2:
374
+ self._addTextPar(xText, S_HEAD2, oStyle, tText, isHead=True, oLevel="2")
501
375
 
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")
376
+ elif tType == BlockTyp.HEAD3:
377
+ self._addTextPar(xText, S_HEAD3, oStyle, tText, isHead=True, oLevel="3")
505
378
 
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")
379
+ elif tType == BlockTyp.HEAD4:
380
+ self._addTextPar(xText, S_HEAD4, oStyle, tText, isHead=True, oLevel="4")
509
381
 
510
- elif tType == self.T_SEP:
382
+ elif tType == BlockTyp.SEP:
511
383
  self._addTextPar(xText, S_SEP, oStyle, tText)
512
384
 
513
- elif tType == self.T_SKIP:
385
+ elif tType == BlockTyp.SKIP:
514
386
  self._addTextPar(xText, S_TEXT, oStyle, "")
515
387
 
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)
388
+ elif tType == BlockTyp.COMMENT:
389
+ self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat)
523
390
 
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)
391
+ elif tType == BlockTyp.KEYWORD:
392
+ self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat)
531
393
 
532
394
  return
533
395
 
534
396
  def closeDocument(self) -> None:
535
- """Pack the automatic styles of the XML document."""
397
+ """Add additional collected information to the XML."""
536
398
  for style in self._autoPara.values():
537
399
  style.packXML(self._xAuto)
538
400
  for style in self._autoText.values():
539
401
  style.packXML(self._xAuto)
402
+ if self._counts:
403
+ xFields = ET.Element(_mkTag("text", "user-field-decls"))
404
+ for key, value in self._counts.items():
405
+ ET.SubElement(xFields, _mkTag("text", "user-field-decl"), attrib={
406
+ _mkTag("office", "value-type"): "float",
407
+ _mkTag("office", "value"): str(value),
408
+ _mkTag("text", "name"): f"Manuscript{key[:1].upper()}{key[1:]}",
409
+ })
410
+ self._xText.insert(0, xFields)
540
411
  return
541
412
 
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)
413
+ def saveDocument(self, path: Path) -> None:
414
+ """Save the data to an .fodt or .odt file."""
415
+ if self._isFlat:
416
+ with open(path, mode="wb") as fObj:
417
+ xml = ET.ElementTree(self._dFlat)
418
+ xmlIndent(xml)
573
419
  xml.write(fObj, encoding="utf-8", xml_declaration=True)
574
420
 
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)
421
+ else:
422
+ mMani = _mkTag("manifest", "manifest")
423
+ mVers = _mkTag("manifest", "version")
424
+ mPath = _mkTag("manifest", "full-path")
425
+ mType = _mkTag("manifest", "media-type")
426
+ mFile = _mkTag("manifest", "file-entry")
427
+
428
+ xMani = ET.Element(mMani, attrib={mVers: X_VERS})
429
+ ET.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
430
+ ET.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
431
+ ET.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
432
+ ET.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
433
+ ET.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
434
+
435
+ oRoot = _mkTag("office", "document-settings")
436
+ oVers = _mkTag("office", "version")
437
+ xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
438
+
439
+ def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
440
+ zipObj.writestr(
441
+ name, ET.tostring(root, encoding="utf-8", xml_declaration=True),
442
+ compress_type=ZIP_DEFLATED, compresslevel=3,
443
+ )
444
+
445
+ with ZipFile(path, mode="w") as outZip:
446
+ outZip.writestr("mimetype", X_MIME, compress_type=None, compresslevel=None)
447
+ xmlToZip("META-INF/manifest.xml", xMani, outZip)
448
+ xmlToZip("settings.xml", xSett, outZip)
449
+ xmlToZip("content.xml", self._dCont, outZip)
450
+ xmlToZip("meta.xml", self._dMeta, outZip)
451
+ xmlToZip("styles.xml", self._dStyl, outZip)
582
452
 
583
453
  logger.info("Wrote file: %s", path)
584
454
 
@@ -588,43 +458,14 @@ class ToOdt(Tokenizer):
588
458
  # Internal Functions
589
459
  ##
590
460
 
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
461
  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
462
+ self,
463
+ xParent: ET.Element,
464
+ styleName: str, oStyle: ODTParagraphStyle,
465
+ tText: str,
466
+ tFmt: Sequence[tuple[int, int, str]] | None = None,
467
+ isHead: bool = False,
468
+ oLevel: str | None = None,
628
469
  ) -> None:
629
470
  """Add a text paragraph to the text XML element."""
630
471
  tAttr = {_mkTag("text", "style-name"): self._paraStyle(styleName, oStyle)}
@@ -646,12 +487,13 @@ class ToOdt(Tokenizer):
646
487
 
647
488
  parProc = XMLParagraph(xElem)
648
489
 
649
- pErr = 0
650
490
  xFmt = 0x00
651
491
  tFrag = ""
652
492
  fLast = 0
653
493
  xNode = None
654
- for fPos, fFmt, fData in tFmt:
494
+ fClass = ""
495
+ fLink = ""
496
+ for fPos, fFmt, fData in tFmt or []:
655
497
 
656
498
  # Add any extra nodes
657
499
  if xNode is not None:
@@ -663,51 +505,55 @@ class ToOdt(Tokenizer):
663
505
  if xFmt == 0x00:
664
506
  parProc.appendText(tFrag)
665
507
  else:
666
- parProc.appendSpan(tFrag, self._textStyle(xFmt))
508
+ parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass), fLink)
667
509
 
668
510
  # Calculate the change of format
669
- if fFmt == self.FMT_B_B:
511
+ if fFmt == TextFmt.B_B:
670
512
  xFmt |= X_BLD
671
- elif fFmt == self.FMT_B_E:
513
+ elif fFmt == TextFmt.B_E:
672
514
  xFmt &= M_BLD
673
- elif fFmt == self.FMT_I_B:
515
+ elif fFmt == TextFmt.I_B:
674
516
  xFmt |= X_ITA
675
- elif fFmt == self.FMT_I_E:
517
+ elif fFmt == TextFmt.I_E:
676
518
  xFmt &= M_ITA
677
- elif fFmt == self.FMT_D_B:
519
+ elif fFmt == TextFmt.D_B:
678
520
  xFmt |= X_DEL
679
- elif fFmt == self.FMT_D_E:
521
+ elif fFmt == TextFmt.D_E:
680
522
  xFmt &= M_DEL
681
- elif fFmt == self.FMT_U_B:
523
+ elif fFmt == TextFmt.U_B:
682
524
  xFmt |= X_UND
683
- elif fFmt == self.FMT_U_E:
525
+ elif fFmt == TextFmt.U_E:
684
526
  xFmt &= M_UND
685
- elif fFmt == self.FMT_M_B:
527
+ elif fFmt == TextFmt.M_B:
686
528
  xFmt |= X_MRK
687
- elif fFmt == self.FMT_M_E:
529
+ elif fFmt == TextFmt.M_E:
688
530
  xFmt &= M_MRK
689
- elif fFmt == self.FMT_SUP_B:
531
+ elif fFmt == TextFmt.SUP_B:
690
532
  xFmt |= X_SUP
691
- elif fFmt == self.FMT_SUP_E:
533
+ elif fFmt == TextFmt.SUP_E:
692
534
  xFmt &= M_SUP
693
- elif fFmt == self.FMT_SUB_B:
535
+ elif fFmt == TextFmt.SUB_B:
694
536
  xFmt |= X_SUB
695
- elif fFmt == self.FMT_SUB_E:
537
+ elif fFmt == TextFmt.SUB_E:
696
538
  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:
539
+ elif fFmt == TextFmt.COL_B:
540
+ xFmt |= X_COL
541
+ fClass = fData
542
+ elif fFmt == TextFmt.COL_E:
543
+ xFmt &= M_COL
544
+ fClass = ""
545
+ elif fFmt == TextFmt.HRF_B:
546
+ xFmt |= X_HRF
547
+ fLink = fData
548
+ elif fFmt == TextFmt.HRF_E:
549
+ xFmt &= M_HRF
550
+ fLink = ""
551
+ elif fFmt == TextFmt.FNOTE:
706
552
  xNode = self._generateFootnote(fData)
707
- elif fFmt == self.FMT_STRIP:
553
+ elif fFmt == TextFmt.FIELD:
554
+ xNode = self._generateField(fData, xFmt)
555
+ elif fFmt == TextFmt.STRIP:
708
556
  pass
709
- else:
710
- pErr += 1
711
557
 
712
558
  fLast = fPos
713
559
 
@@ -718,10 +564,7 @@ class ToOdt(Tokenizer):
718
564
  if xFmt == 0x00:
719
565
  parProc.appendText(tFrag)
720
566
  else:
721
- parProc.appendSpan(tFrag, self._textStyle(xFmt))
722
-
723
- if pErr > 0:
724
- self._errData.append("Unknown format tag encountered")
567
+ parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass), fLink)
725
568
 
726
569
  nErr, errMsg = parProc.checkError()
727
570
  if nErr > 0: # pragma: no cover
@@ -752,10 +595,13 @@ class ToOdt(Tokenizer):
752
595
 
753
596
  return modStyle.name
754
597
 
755
- def _textStyle(self, hFmt: int) -> str:
598
+ def _textStyle(self, hFmt: int, fClass: str = "") -> str:
756
599
  """Return a text style for a given style code."""
757
- if hFmt in self._autoText:
758
- return self._autoText[hFmt].name
600
+ tKey = str(hFmt)
601
+ if fClass and (color := self._classes.get(fClass)):
602
+ tKey = f"{tKey}:{fClass}"
603
+ if tKey in self._autoText:
604
+ return self._autoText[tKey].name
759
605
 
760
606
  style = ODTTextStyle(f"T{len(self._autoText)+1:d}")
761
607
  if hFmt & X_BLD:
@@ -768,18 +614,21 @@ class ToOdt(Tokenizer):
768
614
  if hFmt & X_UND:
769
615
  style.setUnderlineStyle("solid")
770
616
  style.setUnderlineWidth("auto")
771
- style.setUnderlineColour("font-color")
617
+ style.setUnderlineColor("font-color")
772
618
  if hFmt & X_MRK:
773
- style.setBackgroundColour(self._markText)
619
+ style.setBackgroundColor(self._theme.highlight)
774
620
  if hFmt & X_SUP:
775
621
  style.setTextPosition("super")
776
622
  if hFmt & X_SUB:
777
623
  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
624
+ if hFmt & X_COL and color:
625
+ style.setColor(color)
626
+ if hFmt & X_HRF:
627
+ style.setColor(self._theme.link)
628
+ style.setUnderlineStyle("solid")
629
+ style.setUnderlineWidth("auto")
630
+ style.setUnderlineColor("font-color")
631
+ self._autoText[tKey] = style
783
632
 
784
633
  return style.name
785
634
 
@@ -788,20 +637,40 @@ class ToOdt(Tokenizer):
788
637
  if content := self._footnotes.get(key):
789
638
  self._nNote += 1
790
639
  nStyle = ODTParagraphStyle("New")
791
- xNote = ET.Element(_mkTag("text", "note"), attrib={
640
+ xNote = xmlElement(_mkTag("text", "note"), attrib={
792
641
  _mkTag("text", "id"): f"ftn{self._nNote}",
793
642
  _mkTag("text", "note-class"): "footnote",
794
643
  })
795
- xCite = ET.SubElement(xNote, _mkTag("text", "note-citation"))
796
- xCite.text = str(self._nNote)
797
- xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
644
+ xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote)
645
+ xBody = xmlSubElem(xNote, _mkTag("text", "note-body"))
798
646
  self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
799
647
  return xNote
800
648
  return None
801
649
 
650
+ def _generateField(self, key: str, fmt: int) -> ET.Element | None:
651
+ """Generate a data field XML object."""
652
+ if key and (field := key.partition(":")[2]):
653
+ xField = xmlElement(_mkTag("text", "user-field-get"), "0", tail="", attrib={
654
+ _mkTag("style", "data-style-name"): S_NUM,
655
+ _mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}",
656
+ })
657
+ if fmt == 0x00:
658
+ return xField
659
+ else:
660
+ xSpan = xmlElement(TAG_SPAN, "", tail="", attrib={
661
+ _mkTag("text", "style-name"): self._textStyle(fmt),
662
+ })
663
+ xSpan.append(xField)
664
+ return xSpan
665
+ return None
666
+
802
667
  def _emToCm(self, value: float) -> str:
803
668
  """Converts an em value to centimetres."""
804
- return f"{value*2.54/72*self._fontSize:.3f}cm"
669
+ return f"{value*self._fontSize*2.54/72.0:.3f}cm"
670
+
671
+ def _emToPt(self, scale: float) -> str:
672
+ """Compute relative font size in points."""
673
+ return f"{round(scale * self._fontSize):d}pt"
805
674
 
806
675
  ##
807
676
  # Style Elements
@@ -825,16 +694,19 @@ class ToOdt(Tokenizer):
825
694
 
826
695
  xHead = ET.SubElement(xPage, _mkTag("style", "header-style"))
827
696
  ET.SubElement(xHead, _mkTag("style", "header-footer-properties"), attrib={
828
- _mkTag("fo", "min-height"): "0.600cm",
697
+ _mkTag("fo", "min-height"): self._emToCm(1.5),
829
698
  _mkTag("fo", "margin-left"): "0.000cm",
830
699
  _mkTag("fo", "margin-right"): "0.000cm",
831
- _mkTag("fo", "margin-bottom"): "0.500cm",
700
+ _mkTag("fo", "margin-bottom"): self._emToCm(0.5),
832
701
  })
833
702
 
834
703
  return
835
704
 
836
705
  def _defaultStyles(self) -> None:
837
706
  """Set the default styles."""
707
+ hScale = self._scaleHeads
708
+ textSize = self._emToPt(nwStyles.T_NORMAL)
709
+
838
710
  # Add Paragraph Family Style
839
711
  xStyl = ET.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib={
840
712
  _mkTag("style", "family"): "paragraph",
@@ -849,7 +721,7 @@ class ToOdt(Tokenizer):
849
721
  _mkTag("fo", "font-family"): self._fontFamily,
850
722
  _mkTag("fo", "font-weight"): self._fontWeight,
851
723
  _mkTag("fo", "font-style"): self._fontStyle,
852
- _mkTag("fo", "font-size"): self._fSizeText,
724
+ _mkTag("fo", "font-size"): textSize,
853
725
  _mkTag("fo", "language"): self._dLanguage,
854
726
  _mkTag("fo", "country"): self._dCountry,
855
727
  })
@@ -865,7 +737,7 @@ class ToOdt(Tokenizer):
865
737
  _mkTag("fo", "font-family"): self._fontFamily,
866
738
  _mkTag("fo", "font-weight"): self._fontWeight,
867
739
  _mkTag("fo", "font-style"): self._fontStyle,
868
- _mkTag("fo", "font-size"): self._fSizeText,
740
+ _mkTag("fo", "font-size"): textSize,
869
741
  })
870
742
 
871
743
  # Add Default Heading Style
@@ -877,8 +749,8 @@ class ToOdt(Tokenizer):
877
749
  _mkTag("style", "class"): "text",
878
750
  })
879
751
  ET.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib={
880
- _mkTag("fo", "margin-top"): self._mTopHead,
881
- _mkTag("fo", "margin-bottom"): self._mBotHead,
752
+ _mkTag("fo", "margin-top"): self._emToCm(self._marginHead4[0]),
753
+ _mkTag("fo", "margin-bottom"): self._emToCm(self._marginHead4[1]),
882
754
  _mkTag("fo", "keep-with-next"): "always",
883
755
  })
884
756
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
@@ -886,7 +758,7 @@ class ToOdt(Tokenizer):
886
758
  _mkTag("fo", "font-family"): self._fontFamily,
887
759
  _mkTag("fo", "font-weight"): self._fontWeight,
888
760
  _mkTag("fo", "font-style"): self._fontStyle,
889
- _mkTag("fo", "font-size"): self._fSizeHead,
761
+ _mkTag("fo", "font-size"): self._emToPt(nwStyles.H_SIZES[4] if hScale else 1.0),
890
762
  })
891
763
 
892
764
  # Add Header and Footer Styles
@@ -898,22 +770,36 @@ class ToOdt(Tokenizer):
898
770
  _mkTag("style", "class"): "extra",
899
771
  })
900
772
 
773
+ # Numbers Style
774
+ xStyl = ET.SubElement(self._xStyl, _mkTag("number", "number-style"), attrib={
775
+ _mkTag("style", "name"): S_NUM,
776
+ })
777
+ ET.SubElement(xStyl, _mkTag("number", "number"), attrib={
778
+ _mkTag("number", "min-integer-digits"): "1",
779
+ })
780
+
901
781
  return
902
782
 
903
783
  def _useableStyles(self) -> None:
904
784
  """Set the usable styles."""
785
+ hScale = self._scaleHeads
786
+ hColor = self._theme.head if self._colorHeads else None
787
+
788
+ textSize = self._emToPt(nwStyles.T_NORMAL)
789
+ lineHeight = f"{round(100 * self._lineHeight):d}%"
790
+
905
791
  # Add Text Body Style
906
792
  style = ODTParagraphStyle(S_TEXT)
907
793
  style.setDisplayName("Text body")
908
794
  style.setParentStyleName("Standard")
909
795
  style.setClass("text")
910
- style.setMarginTop(self._mTopText)
911
- style.setMarginBottom(self._mBotText)
912
- style.setLineHeight(self._fLineHeight)
913
- style.setTextAlign(self._textAlign)
796
+ style.setMarginTop(self._emToCm(self._marginText[0]))
797
+ style.setMarginBottom(self._emToCm(self._marginText[1]))
798
+ style.setLineHeight(lineHeight)
799
+ style.setTextAlign(self._defaultAlign)
914
800
  style.setFontName(self._fontFamily)
915
801
  style.setFontFamily(self._fontFamily)
916
- style.setFontSize(self._fSizeText)
802
+ style.setFontSize(textSize)
917
803
  style.setFontWeight(self._fontWeight)
918
804
  style.packXML(self._xStyl)
919
805
  self._mainPara[style.name] = style
@@ -923,7 +809,7 @@ class ToOdt(Tokenizer):
923
809
  style.setDisplayName("First line indent")
924
810
  style.setParentStyleName(S_TEXT)
925
811
  style.setClass("text")
926
- style.setTextIndent(self._fTextIndent)
812
+ style.setTextIndent(self._emToCm(self._firstWidth))
927
813
  style.packXML(self._xStyl)
928
814
  self._mainPara[style.name] = style
929
815
 
@@ -932,15 +818,13 @@ class ToOdt(Tokenizer):
932
818
  style.setDisplayName("Text Meta")
933
819
  style.setParentStyleName("Standard")
934
820
  style.setClass("text")
935
- style.setMarginTop(self._mTopMeta)
936
- style.setMarginBottom(self._mBotMeta)
937
- style.setLineHeight(self._fLineHeight)
821
+ style.setMarginTop(self._emToCm(self._marginMeta[0]))
822
+ style.setMarginBottom(self._emToCm(self._marginMeta[1]))
823
+ style.setLineHeight(lineHeight)
938
824
  style.setFontName(self._fontFamily)
939
825
  style.setFontFamily(self._fontFamily)
940
- style.setFontSize(self._fSizeText)
826
+ style.setFontSize(textSize)
941
827
  style.setFontWeight(self._fontWeight)
942
- style.setColour(self._colMetaTx)
943
- style.setOpacity(self._opaMetaTx)
944
828
  style.packXML(self._xStyl)
945
829
  self._mainPara[style.name] = style
946
830
 
@@ -950,13 +834,13 @@ class ToOdt(Tokenizer):
950
834
  style.setParentStyleName("Heading")
951
835
  style.setNextStyleName(S_TEXT)
952
836
  style.setClass("chapter")
953
- style.setMarginTop(self._mTopTitle)
954
- style.setMarginBottom(self._mBotTitle)
837
+ style.setMarginTop(self._emToCm(self._marginTitle[0]))
838
+ style.setMarginBottom(self._emToCm(self._marginTitle[1]))
955
839
  style.setTextAlign("center")
956
840
  style.setFontName(self._fontFamily)
957
841
  style.setFontFamily(self._fontFamily)
958
- style.setFontSize(self._fSizeTitle)
959
- style.setFontWeight(self._fontBold)
842
+ style.setFontSize(self._emToPt(nwStyles.H_SIZES[0] if hScale else 1.0))
843
+ style.setFontWeight(self._headWeight)
960
844
  style.packXML(self._xStyl)
961
845
  self._mainPara[style.name] = style
962
846
 
@@ -966,13 +850,13 @@ class ToOdt(Tokenizer):
966
850
  style.setParentStyleName("Standard")
967
851
  style.setNextStyleName(S_TEXT)
968
852
  style.setClass("text")
969
- style.setMarginTop(self._mTopSep)
970
- style.setMarginBottom(self._mBotSep)
971
- style.setLineHeight(self._fLineHeight)
853
+ style.setMarginTop(self._emToCm(self._marginSep[0]))
854
+ style.setMarginBottom(self._emToCm(self._marginSep[1]))
855
+ style.setLineHeight(lineHeight)
972
856
  style.setTextAlign("center")
973
857
  style.setFontName(self._fontFamily)
974
858
  style.setFontFamily(self._fontFamily)
975
- style.setFontSize(self._fSizeText)
859
+ style.setFontSize(textSize)
976
860
  style.setFontWeight(self._fontWeight)
977
861
  style.packXML(self._xStyl)
978
862
  self._mainPara[style.name] = style
@@ -984,14 +868,13 @@ class ToOdt(Tokenizer):
984
868
  style.setNextStyleName(S_TEXT)
985
869
  style.setOutlineLevel("1")
986
870
  style.setClass("text")
987
- style.setMarginTop(self._mTopHead1)
988
- style.setMarginBottom(self._mBotHead1)
871
+ style.setMarginTop(self._emToCm(self._marginHead1[0]))
872
+ style.setMarginBottom(self._emToCm(self._marginHead1[1]))
989
873
  style.setFontName(self._fontFamily)
990
874
  style.setFontFamily(self._fontFamily)
991
- style.setFontSize(self._fSizeHead1)
992
- style.setFontWeight(self._fontBold)
993
- style.setColour(self._colHead12)
994
- style.setOpacity(self._opaHead12)
875
+ style.setFontSize(self._emToPt(nwStyles.H_SIZES[1] if hScale else 1.0))
876
+ style.setFontWeight(self._headWeight)
877
+ style.setColor(hColor)
995
878
  style.packXML(self._xStyl)
996
879
  self._mainPara[style.name] = style
997
880
 
@@ -1002,14 +885,13 @@ class ToOdt(Tokenizer):
1002
885
  style.setNextStyleName(S_TEXT)
1003
886
  style.setOutlineLevel("2")
1004
887
  style.setClass("text")
1005
- style.setMarginTop(self._mTopHead2)
1006
- style.setMarginBottom(self._mBotHead2)
888
+ style.setMarginTop(self._emToCm(self._marginHead2[0]))
889
+ style.setMarginBottom(self._emToCm(self._marginHead2[1]))
1007
890
  style.setFontName(self._fontFamily)
1008
891
  style.setFontFamily(self._fontFamily)
1009
- style.setFontSize(self._fSizeHead2)
1010
- style.setFontWeight(self._fontBold)
1011
- style.setColour(self._colHead12)
1012
- style.setOpacity(self._opaHead12)
892
+ style.setFontSize(self._emToPt(nwStyles.H_SIZES[2] if hScale else 1.0))
893
+ style.setFontWeight(self._headWeight)
894
+ style.setColor(hColor)
1013
895
  style.packXML(self._xStyl)
1014
896
  self._mainPara[style.name] = style
1015
897
 
@@ -1020,14 +902,13 @@ class ToOdt(Tokenizer):
1020
902
  style.setNextStyleName(S_TEXT)
1021
903
  style.setOutlineLevel("3")
1022
904
  style.setClass("text")
1023
- style.setMarginTop(self._mTopHead3)
1024
- style.setMarginBottom(self._mBotHead3)
905
+ style.setMarginTop(self._emToCm(self._marginHead3[0]))
906
+ style.setMarginBottom(self._emToCm(self._marginHead3[1]))
1025
907
  style.setFontName(self._fontFamily)
1026
908
  style.setFontFamily(self._fontFamily)
1027
- style.setFontSize(self._fSizeHead3)
1028
- style.setFontWeight(self._fontBold)
1029
- style.setColour(self._colHead34)
1030
- style.setOpacity(self._opaHead34)
909
+ style.setFontSize(self._emToPt(nwStyles.H_SIZES[3] if hScale else 1.0))
910
+ style.setFontWeight(self._headWeight)
911
+ style.setColor(hColor)
1031
912
  style.packXML(self._xStyl)
1032
913
  self._mainPara[style.name] = style
1033
914
 
@@ -1038,14 +919,13 @@ class ToOdt(Tokenizer):
1038
919
  style.setNextStyleName(S_TEXT)
1039
920
  style.setOutlineLevel("4")
1040
921
  style.setClass("text")
1041
- style.setMarginTop(self._mTopHead4)
1042
- style.setMarginBottom(self._mBotHead4)
922
+ style.setMarginTop(self._emToCm(self._marginHead4[0]))
923
+ style.setMarginBottom(self._emToCm(self._marginHead4[1]))
1043
924
  style.setFontName(self._fontFamily)
1044
925
  style.setFontFamily(self._fontFamily)
1045
- style.setFontSize(self._fSizeHead4)
1046
- style.setFontWeight(self._fontBold)
1047
- style.setColour(self._colHead34)
1048
- style.setOpacity(self._opaHead34)
926
+ style.setFontSize(self._emToPt(nwStyles.H_SIZES[4] if hScale else 1.0))
927
+ style.setFontWeight(self._headWeight)
928
+ style.setColor(hColor)
1049
929
  style.packXML(self._xStyl)
1050
930
  self._mainPara[style.name] = style
1051
931
 
@@ -1062,10 +942,10 @@ class ToOdt(Tokenizer):
1062
942
  style.setDisplayName("Footnote")
1063
943
  style.setParentStyleName("Standard")
1064
944
  style.setClass("extra")
1065
- style.setMarginLeft(self._mLeftFoot)
1066
- style.setMarginBottom(self._mBotFoot)
1067
- style.setTextIndent("-"+self._mLeftFoot)
1068
- style.setFontSize(self._fSizeFoot)
945
+ style.setMarginLeft(self._emToCm(self._marginFoot[0]))
946
+ style.setMarginBottom(self._emToCm(self._marginFoot[1]))
947
+ style.setTextIndent("-"+self._emToCm(self._marginFoot[0]))
948
+ style.setFontSize(self._emToPt(nwStyles.T_SMALL))
1069
949
  style.packXML(self._xStyl)
1070
950
  self._mainPara[style.name] = style
1071
951
 
@@ -1080,12 +960,12 @@ class ToOdt(Tokenizer):
1080
960
 
1081
961
  # Standard Page Header
1082
962
  if self._headerFormat:
1083
- pre, page, post = self._headerFormat.partition(nwHeadFmt.ODT_PAGE)
963
+ pre, page, post = self._headerFormat.partition(nwHeadFmt.DOC_PAGE)
1084
964
 
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)
965
+ pre = pre.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
966
+ pre = pre.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
967
+ post = post.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
968
+ post = post.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
1089
969
 
1090
970
  xHead = ET.SubElement(xPage, _mkTag("style", "header"))
1091
971
  xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
@@ -1167,16 +1047,6 @@ class ODTParagraphStyle:
1167
1047
  def name(self) -> str:
1168
1048
  return self._name
1169
1049
 
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
1050
  ##
1181
1051
  # Setters
1182
1052
  ##
@@ -1306,14 +1176,14 @@ class ODTParagraphStyle:
1306
1176
  self._tAttr["font-weight"][1] = None
1307
1177
  return
1308
1178
 
1309
- def setColour(self, value: str | None) -> None:
1179
+ def setColor(self, value: QColor | None) -> None:
1310
1180
  """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
1181
+ if isinstance(value, QColor):
1182
+ self._tAttr["color"][1] = value.name(QtHexRgb)
1183
+ self._tAttr["opacity"][1] = f"{int(100.0 * value.alphaF())}%"
1184
+ else:
1185
+ self._tAttr["color"][1] = None
1186
+ self._tAttr["opacity"][1] = None
1317
1187
  return
1318
1188
 
1319
1189
  ##
@@ -1414,18 +1284,18 @@ class ODTTextStyle:
1414
1284
  self._tAttr["font-style"][1] = None
1415
1285
  return
1416
1286
 
1417
- def setColour(self, value: str | None) -> None:
1287
+ def setColor(self, value: QColor | None) -> None:
1418
1288
  """Set text colour."""
1419
- if value and len(value) == 7 and value[0] == "#":
1420
- self._tAttr["color"][1] = value
1289
+ if isinstance(value, QColor):
1290
+ self._tAttr["color"][1] = value.name(QtHexRgb)
1421
1291
  else:
1422
1292
  self._tAttr["color"][1] = None
1423
1293
  return
1424
1294
 
1425
- def setBackgroundColour(self, value: str | None) -> None:
1295
+ def setBackgroundColor(self, value: QColor | None) -> None:
1426
1296
  """Set text background colour."""
1427
- if value and len(value) == 7 and value[0] == "#":
1428
- self._tAttr["background-color"][1] = value
1297
+ if isinstance(value, QColor):
1298
+ self._tAttr["background-color"][1] = value.name(QtHexRgb)
1429
1299
  else:
1430
1300
  self._tAttr["background-color"][1] = None
1431
1301
  return
@@ -1470,7 +1340,7 @@ class ODTTextStyle:
1470
1340
  self._tAttr["text-underline-width"][1] = None
1471
1341
  return
1472
1342
 
1473
- def setUnderlineColour(self, value: str | None) -> None:
1343
+ def setUnderlineColor(self, value: str | None) -> None:
1474
1344
  """Set text underline colour."""
1475
1345
  if value in self.VALID_LCOL:
1476
1346
  self._tAttr["text-underline-color"][1] = value
@@ -1595,13 +1465,22 @@ class XMLParagraph:
1595
1465
 
1596
1466
  return
1597
1467
 
1598
- def appendSpan(self, text: str, fmt: str) -> None:
1468
+ def appendSpan(self, text: str, style: str, link: str) -> None:
1599
1469
  """Append a text span to the XML element. The span is always
1600
- closed since we do not allow nested spans (like Libre Office).
1470
+ closed since we do not produce nested spans (like Libre Office).
1601
1471
  Therefore we return to the root element level when we're done
1602
1472
  processing the text of the span.
1603
1473
  """
1604
- self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: fmt})
1474
+ if link:
1475
+ self._xTail = ET.SubElement(self._xRoot, _mkTag("text", "a"), attrib={
1476
+ _mkTag("xlink", "type"): "simple",
1477
+ _mkTag("xlink", "href"): link,
1478
+ _mkTag("text", "style-name"): style,
1479
+ })
1480
+ else:
1481
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={
1482
+ _mkTag("text", "style-name"): style,
1483
+ })
1605
1484
  self._xTail.text = "" # Defaults to None
1606
1485
  self._xTail.tail = "" # Defaults to None
1607
1486
  self._nState = X_SPAN_TEXT