novelWriter 2.4.3__py3-none-any.whl → 2.5b1__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 (109) hide show
  1. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/RECORD +109 -101
  3. novelwriter/__init__.py +33 -39
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  6. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  9. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  11. novelwriter/assets/manual.pdf +0 -0
  12. novelwriter/assets/sample.zip +0 -0
  13. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  14. novelwriter/assets/syntax/default_dark.conf +32 -18
  15. novelwriter/assets/syntax/default_light.conf +24 -10
  16. novelwriter/assets/syntax/dracula.conf +44 -0
  17. novelwriter/assets/syntax/grey_dark.conf +5 -4
  18. novelwriter/assets/syntax/grey_light.conf +5 -4
  19. novelwriter/assets/syntax/light_owl.conf +7 -6
  20. novelwriter/assets/syntax/night_owl.conf +7 -6
  21. novelwriter/assets/syntax/snazzy.conf +42 -0
  22. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  23. novelwriter/assets/syntax/solarized_light.conf +4 -3
  24. novelwriter/assets/syntax/tango.conf +27 -11
  25. novelwriter/assets/syntax/tomorrow.conf +6 -5
  26. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  27. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  28. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  29. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  30. novelwriter/assets/text/credits_en.htm +4 -1
  31. novelwriter/assets/themes/cyberpunk_night.conf +2 -0
  32. novelwriter/assets/themes/default_dark.conf +1 -0
  33. novelwriter/assets/themes/default_light.conf +1 -0
  34. novelwriter/assets/themes/dracula.conf +47 -0
  35. novelwriter/assets/themes/solarized_dark.conf +1 -0
  36. novelwriter/assets/themes/solarized_light.conf +1 -0
  37. novelwriter/common.py +31 -9
  38. novelwriter/config.py +118 -84
  39. novelwriter/constants.py +40 -26
  40. novelwriter/core/buildsettings.py +63 -66
  41. novelwriter/core/coretools.py +2 -22
  42. novelwriter/core/docbuild.py +51 -40
  43. novelwriter/core/document.py +3 -5
  44. novelwriter/core/index.py +115 -45
  45. novelwriter/core/item.py +8 -19
  46. novelwriter/core/options.py +2 -4
  47. novelwriter/core/project.py +23 -57
  48. novelwriter/core/projectdata.py +1 -3
  49. novelwriter/core/projectxml.py +12 -15
  50. novelwriter/core/sessions.py +3 -5
  51. novelwriter/core/spellcheck.py +4 -9
  52. novelwriter/core/status.py +211 -164
  53. novelwriter/core/storage.py +0 -8
  54. novelwriter/core/tohtml.py +94 -100
  55. novelwriter/core/tokenizer.py +199 -112
  56. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  57. novelwriter/core/toodt.py +212 -148
  58. novelwriter/core/toqdoc.py +403 -0
  59. novelwriter/core/tree.py +5 -7
  60. novelwriter/dialogs/about.py +3 -5
  61. novelwriter/dialogs/docmerge.py +1 -3
  62. novelwriter/dialogs/docsplit.py +1 -3
  63. novelwriter/dialogs/editlabel.py +0 -2
  64. novelwriter/dialogs/preferences.py +111 -88
  65. novelwriter/dialogs/projectsettings.py +216 -180
  66. novelwriter/dialogs/quotes.py +3 -4
  67. novelwriter/dialogs/wordlist.py +3 -9
  68. novelwriter/enum.py +31 -25
  69. novelwriter/error.py +8 -15
  70. novelwriter/extensions/circularprogress.py +5 -6
  71. novelwriter/extensions/configlayout.py +18 -18
  72. novelwriter/extensions/eventfilters.py +1 -5
  73. novelwriter/extensions/modified.py +50 -13
  74. novelwriter/extensions/novelselector.py +1 -3
  75. novelwriter/extensions/pagedsidebar.py +9 -12
  76. novelwriter/extensions/simpleprogress.py +1 -3
  77. novelwriter/extensions/statusled.py +1 -3
  78. novelwriter/extensions/switch.py +4 -6
  79. novelwriter/extensions/switchbox.py +7 -6
  80. novelwriter/extensions/versioninfo.py +3 -9
  81. novelwriter/gui/doceditor.py +98 -126
  82. novelwriter/gui/dochighlight.py +237 -183
  83. novelwriter/gui/docviewer.py +46 -94
  84. novelwriter/gui/docviewerpanel.py +3 -10
  85. novelwriter/gui/editordocument.py +1 -3
  86. novelwriter/gui/itemdetails.py +7 -11
  87. novelwriter/gui/mainmenu.py +11 -7
  88. novelwriter/gui/noveltree.py +11 -24
  89. novelwriter/gui/outline.py +11 -23
  90. novelwriter/gui/projtree.py +26 -43
  91. novelwriter/gui/search.py +1 -3
  92. novelwriter/gui/sidebar.py +2 -6
  93. novelwriter/gui/statusbar.py +6 -10
  94. novelwriter/gui/theme.py +23 -48
  95. novelwriter/guimain.py +50 -71
  96. novelwriter/shared.py +30 -15
  97. novelwriter/tools/dictionaries.py +8 -12
  98. novelwriter/tools/lipsum.py +2 -4
  99. novelwriter/tools/manusbuild.py +1 -3
  100. novelwriter/tools/manuscript.py +66 -145
  101. novelwriter/tools/manussettings.py +67 -73
  102. novelwriter/tools/noveldetails.py +6 -11
  103. novelwriter/tools/welcome.py +2 -16
  104. novelwriter/tools/writingstats.py +6 -9
  105. novelwriter/types.py +45 -3
  106. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/entry_points.txt +0 -0
  109. {novelWriter-2.4.3.dist-info → novelWriter-2.5b1.dist-info}/top_level.txt +0 -0
novelwriter/core/toodt.py CHANGED
@@ -29,17 +29,20 @@ from __future__ import annotations
29
29
  import logging
30
30
  import xml.etree.ElementTree as ET
31
31
 
32
+ from collections.abc import Sequence
33
+ from datetime import datetime
32
34
  from hashlib import sha256
33
35
  from pathlib import Path
34
36
  from zipfile import ZipFile
35
- from datetime import datetime
36
- from collections.abc import Sequence
37
+
38
+ from PyQt5.QtGui import QFont
37
39
 
38
40
  from novelwriter import __version__
39
41
  from novelwriter.common import xmlIndent
40
42
  from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels
41
43
  from novelwriter.core.project import NWProject
42
- from novelwriter.core.tokenizer import Tokenizer, stripEscape
44
+ from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
45
+ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
43
46
 
44
47
  logger = logging.getLogger(__name__)
45
48
 
@@ -96,6 +99,22 @@ M_MRK = ~X_MRK
96
99
  M_SUP = ~X_SUP
97
100
  M_SUB = ~X_SUB
98
101
 
102
+ # ODT Styles
103
+ S_TITLE = "Title"
104
+ S_HEAD1 = "Heading_20_1"
105
+ S_HEAD2 = "Heading_20_2"
106
+ S_HEAD3 = "Heading_20_3"
107
+ S_HEAD4 = "Heading_20_4"
108
+ S_SEP = "Separator"
109
+ S_FIND = "First_20_line_20_indent"
110
+ S_TEXT = "Text_20_body"
111
+ S_META = "Text_20_Meta"
112
+ S_HNF = "Header_20_and_20_Footer"
113
+
114
+ # Font Data
115
+ FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
116
+ FONT_WEIGHT_MAP = {"400": "normal", "700": "bold"}
117
+
99
118
 
100
119
  class ToOdt(Tokenizer):
101
120
  """Core: Open Document Writer
@@ -130,20 +149,25 @@ class ToOdt(Tokenizer):
130
149
  self._autoPara: dict[str, ODTParagraphStyle] = {} # Auto-generated paragraph styles
131
150
  self._autoText: dict[int, ODTTextStyle] = {} # Auto-generated text styles
132
151
 
152
+ # Footnotes
153
+ self._nNote = 0
154
+ self._etNotes: dict[str, ET.Element] = {} # Generated note elements
155
+
133
156
  self._errData = [] # List of errors encountered
134
157
 
135
158
  # Properties
136
- self._textFont = "Liberation Serif"
137
- self._textSize = 12
138
- self._textFixed = False
159
+ self._textFont = QFont("Liberation Serif", 12)
139
160
  self._colourHead = False
140
- self._firstIndent = False
141
161
  self._headerFormat = ""
142
162
  self._pageOffset = 0
143
163
 
144
164
  # Internal
145
- self._fontFamily = "'Liberation Serif'"
165
+ self._fontFamily = "Liberation Serif"
166
+ self._fontSize = 12
167
+ self._fontWeight = "normal"
168
+ self._fontStyle = "normal"
146
169
  self._fontPitch = "variable"
170
+ self._fontBold = "bold"
147
171
  self._fSizeTitle = "30pt"
148
172
  self._fSizeHead1 = "24pt"
149
173
  self._fSizeHead2 = "20pt"
@@ -151,6 +175,7 @@ class ToOdt(Tokenizer):
151
175
  self._fSizeHead4 = "14pt"
152
176
  self._fSizeHead = "14pt"
153
177
  self._fSizeText = "12pt"
178
+ self._fSizeFoot = "10pt"
154
179
  self._fLineHeight = "115%"
155
180
  self._fBlockIndent = "1.693cm"
156
181
  self._fTextIndent = "0.499cm"
@@ -167,6 +192,7 @@ class ToOdt(Tokenizer):
167
192
  self._mTopHead = "0.423cm"
168
193
  self._mTopText = "0.000cm"
169
194
  self._mTopMeta = "0.000cm"
195
+ self._mTopSep = "0.247cm"
170
196
 
171
197
  self._mBotTitle = "0.212cm"
172
198
  self._mBotHead1 = "0.212cm"
@@ -176,6 +202,10 @@ class ToOdt(Tokenizer):
176
202
  self._mBotHead = "0.212cm"
177
203
  self._mBotText = "0.247cm"
178
204
  self._mBotMeta = "0.106cm"
205
+ self._mBotSep = "0.247cm"
206
+
207
+ self._mBotFoot = "0.106cm"
208
+ self._mLeftFoot = "0.600cm"
179
209
 
180
210
  # Document Size and Margins
181
211
  self._mDocWidth = "21.0cm"
@@ -232,11 +262,6 @@ class ToOdt(Tokenizer):
232
262
  self._pageOffset = offset
233
263
  return
234
264
 
235
- def setFirstLineIndent(self, state: bool) -> None:
236
- """Enable or disable first line indent."""
237
- self._firstIndent = state
238
- return
239
-
240
265
  ##
241
266
  # Class Methods
242
267
  ##
@@ -246,18 +271,25 @@ class ToOdt(Tokenizer):
246
271
  # Initialise Variables
247
272
  # ====================
248
273
 
249
- self._fontFamily = self._textFont
250
- if len(self._textFont.split()) > 1:
251
- self._fontFamily = f"'{self._textFont}'"
252
- self._fontPitch = "fixed" if self._textFixed else "variable"
253
-
254
- self._fSizeTitle = f"{round(2.50 * self._textSize):d}pt"
255
- self._fSizeHead1 = f"{round(2.00 * self._textSize):d}pt"
256
- self._fSizeHead2 = f"{round(1.60 * self._textSize):d}pt"
257
- self._fSizeHead3 = f"{round(1.30 * self._textSize):d}pt"
258
- self._fSizeHead4 = f"{round(1.15 * self._textSize):d}pt"
259
- self._fSizeHead = f"{round(1.15 * self._textSize):d}pt"
260
- self._fSizeText = f"{self._textSize:d}pt"
274
+ intWeight = FONT_WEIGHTS.get(self._textFont.weight(), 400)
275
+ fontWeight = str(intWeight)
276
+ fontBold = str(min(intWeight + 300, 900))
277
+
278
+ self._fontFamily = self._textFont.family()
279
+ self._fontSize = self._textFont.pointSize()
280
+ self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight)
281
+ self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal")
282
+ self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable"
283
+ self._fontBold = FONT_WEIGHT_MAP.get(fontBold, fontBold)
284
+
285
+ self._fSizeTitle = f"{round(2.50 * self._fontSize):d}pt"
286
+ self._fSizeHead1 = f"{round(2.00 * self._fontSize):d}pt"
287
+ self._fSizeHead2 = f"{round(1.60 * self._fontSize):d}pt"
288
+ self._fSizeHead3 = f"{round(1.30 * self._fontSize):d}pt"
289
+ self._fSizeHead4 = f"{round(1.15 * self._fontSize):d}pt"
290
+ self._fSizeHead = f"{round(1.15 * self._fontSize):d}pt"
291
+ self._fSizeText = f"{self._fontSize:d}pt"
292
+ self._fSizeFoot = f"{round(0.8*self._fontSize):d}pt"
261
293
 
262
294
  mScale = self._lineHeight/1.15
263
295
 
@@ -269,6 +301,7 @@ class ToOdt(Tokenizer):
269
301
  self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
270
302
  self._mTopText = self._emToCm(mScale * self._marginText[0])
271
303
  self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
304
+ self._mTopSep = self._emToCm(mScale * self._marginSep[0])
272
305
 
273
306
  self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
274
307
  self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
@@ -278,6 +311,10 @@ class ToOdt(Tokenizer):
278
311
  self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
279
312
  self._mBotText = self._emToCm(mScale * self._marginText[1])
280
313
  self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
314
+ self._mBotSep = self._emToCm(mScale * self._marginSep[1])
315
+
316
+ self._mLeftFoot = self._emToCm(self._marginFoot[0])
317
+ self._mBotFoot = self._emToCm(self._marginFoot[1])
281
318
 
282
319
  if self._colourHead:
283
320
  self._colHead12 = "#2a6099"
@@ -289,6 +326,7 @@ class ToOdt(Tokenizer):
289
326
 
290
327
  self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
291
328
  self._fBlockIndent = self._emToCm(self._blockIndent)
329
+ self._fTextIndent = self._emToCm(self._firstWidth)
292
330
  self._textAlign = "justify" if self._doJustify else "left"
293
331
 
294
332
  # Clear Errors
@@ -301,7 +339,7 @@ class ToOdt(Tokenizer):
301
339
  tAttr[_mkTag("office", "version")] = X_VERS
302
340
 
303
341
  fAttr = {}
304
- fAttr[_mkTag("style", "name")] = self._textFont
342
+ fAttr[_mkTag("style", "name")] = self._fontFamily
305
343
  fAttr[_mkTag("style", "font-pitch")] = self._fontPitch
306
344
 
307
345
  if self._isFlat:
@@ -399,9 +437,7 @@ class ToOdt(Tokenizer):
399
437
  """Convert the list of text tokens into XML elements."""
400
438
  self._result = "" # Not used, but cleared just in case
401
439
 
402
- pFmt = []
403
- pText = []
404
- pStyle = None
440
+ xText = self._xText
405
441
  pIndent = True
406
442
  for tType, _, tText, tFormat, tStyle in self._tokens:
407
443
 
@@ -433,82 +469,59 @@ class ToOdt(Tokenizer):
433
469
  if tStyle & self.A_IND_R:
434
470
  oStyle.setMarginRight(self._fBlockIndent)
435
471
 
436
- if tType not in (self.T_EMPTY, self.T_TEXT):
472
+ if not self._indentFirst and tType in self.L_SKIP_INDENT:
437
473
  pIndent = False
438
474
 
439
475
  # Process Text Types
440
- if tType == self.T_EMPTY:
441
- if len(pText) > 1 and pStyle is not None:
442
- if self._doJustify:
443
- pStyle.setTextAlign("left")
444
-
445
- if len(pText) > 0 and pStyle is not None:
446
- tTxt = ""
447
- tFmt = []
448
- for nText, nFmt in zip(pText, pFmt):
449
- tLen = len(tTxt)
450
- tTxt += f"{nText}\n"
451
- tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
452
-
453
- # Don't indent a paragraph if it has alignment set
454
- tIndent = self._firstIndent and pIndent and pStyle.isUnaligned()
455
- self._addTextPar(
456
- "First_20_line_20_indent" if tIndent else "Text_20_body",
457
- pStyle, tTxt.rstrip(), tFmt=tFmt
458
- )
459
- pIndent = True
460
-
461
- pFmt = []
462
- pText = []
463
- pStyle = None
476
+ if tType == self.T_TEXT:
477
+ if self._firstIndent and pIndent and oStyle.isUnaligned():
478
+ self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
479
+ else:
480
+ self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
481
+ pIndent = True
464
482
 
465
483
  elif tType == self.T_TITLE:
484
+ # Title must be text:p
466
485
  tHead = tText.replace(nwHeadFmt.BR, "\n")
467
- self._addTextPar("Title", oStyle, tHead, isHead=False) # Title must be text:p
486
+ self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
468
487
 
469
488
  elif tType == self.T_HEAD1:
470
489
  tHead = tText.replace(nwHeadFmt.BR, "\n")
471
- self._addTextPar("Heading_20_1", oStyle, tHead, isHead=True, oLevel="1")
490
+ self._addTextPar(xText, S_HEAD1, oStyle, tHead, isHead=True, oLevel="1")
472
491
 
473
492
  elif tType == self.T_HEAD2:
474
493
  tHead = tText.replace(nwHeadFmt.BR, "\n")
475
- self._addTextPar("Heading_20_2", oStyle, tHead, isHead=True, oLevel="2")
494
+ self._addTextPar(xText, S_HEAD2, oStyle, tHead, isHead=True, oLevel="2")
476
495
 
477
496
  elif tType == self.T_HEAD3:
478
497
  tHead = tText.replace(nwHeadFmt.BR, "\n")
479
- self._addTextPar("Heading_20_3", oStyle, tHead, isHead=True, oLevel="3")
498
+ self._addTextPar(xText, S_HEAD3, oStyle, tHead, isHead=True, oLevel="3")
480
499
 
481
500
  elif tType == self.T_HEAD4:
482
501
  tHead = tText.replace(nwHeadFmt.BR, "\n")
483
- self._addTextPar("Heading_20_4", oStyle, tHead, isHead=True, oLevel="4")
502
+ self._addTextPar(xText, S_HEAD4, oStyle, tHead, isHead=True, oLevel="4")
484
503
 
485
504
  elif tType == self.T_SEP:
486
- self._addTextPar("Separator", oStyle, tText)
505
+ self._addTextPar(xText, S_SEP, oStyle, tText)
487
506
 
488
507
  elif tType == self.T_SKIP:
489
- self._addTextPar("Separator", oStyle, "")
490
-
491
- elif tType == self.T_TEXT:
492
- if pStyle is None:
493
- pStyle = oStyle
494
- pText.append(tText)
495
- pFmt.append(tFormat)
508
+ self._addTextPar(xText, S_TEXT, oStyle, "")
496
509
 
497
510
  elif tType == self.T_SYNOPSIS and self._doSynopsis:
498
- tTemp, fTemp = self._formatSynopsis(tText, True)
499
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
511
+ tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
512
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
500
513
 
501
514
  elif tType == self.T_SHORT and self._doSynopsis:
502
- tTemp, fTemp = self._formatSynopsis(tText, False)
503
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
515
+ tTemp, tFmt = self._formatSynopsis(tText, tFormat, False)
516
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
504
517
 
505
518
  elif tType == self.T_COMMENT and self._doComments:
506
- tTemp, fTemp = self._formatComments(tText)
507
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
519
+ tTemp, tFmt = self._formatComments(tText, tFormat)
520
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
508
521
 
509
522
  elif tType == self.T_KEYWORD and self._doKeywords:
510
- tTemp, fTemp = self._formatKeywords(tText)
511
- self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
523
+ tTemp, tFmt = self._formatKeywords(tText)
524
+ self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
512
525
 
513
526
  return
514
527
 
@@ -569,28 +582,32 @@ class ToOdt(Tokenizer):
569
582
  # Internal Functions
570
583
  ##
571
584
 
572
- def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
585
+ def _formatSynopsis(self, text: str, fmt: T_Formats, synopsis: bool) -> tuple[str, T_Formats]:
573
586
  """Apply formatting to synopsis lines."""
574
587
  name = self._localLookup("Synopsis" if synopsis else "Short Description")
588
+ shift = len(name) + 2
575
589
  rTxt = f"{name}: {text}"
576
- rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
590
+ rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(name) + 1, self.FMT_B_E, "")]
591
+ rFmt.extend((p + shift, f, d) for p, f, d in fmt)
577
592
  return rTxt, rFmt
578
593
 
579
- def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
594
+ def _formatComments(self, text: str, fmt: T_Formats) -> tuple[str, T_Formats]:
580
595
  """Apply formatting to comments."""
581
596
  name = self._localLookup("Comment")
597
+ shift = len(name) + 2
582
598
  rTxt = f"{name}: {text}"
583
- rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
599
+ rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(name) + 1, self.FMT_B_E, "")]
600
+ rFmt.extend((p + shift, f, d) for p, f, d in fmt)
584
601
  return rTxt, rFmt
585
602
 
586
- def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
603
+ def _formatKeywords(self, text: str) -> tuple[str, T_Formats]:
587
604
  """Apply formatting to keywords."""
588
605
  valid, bits, _ = self._project.index.scanThis("@"+text)
589
606
  if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
590
607
  return "", []
591
608
 
592
609
  rTxt = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
593
- rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
610
+ rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(rTxt) - 1, self.FMT_B_E, "")]
594
611
  if len(bits) > 1:
595
612
  if bits[0] == nwKeyWords.TAG_KEY:
596
613
  rTxt += bits[1]
@@ -600,8 +617,8 @@ class ToOdt(Tokenizer):
600
617
  return rTxt, rFmt
601
618
 
602
619
  def _addTextPar(
603
- self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
604
- tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
620
+ self, xParent: ET.Element, styleName: str, oStyle: ODTParagraphStyle, tText: str,
621
+ tFmt: Sequence[tuple[int, int, str]] = [], isHead: bool = False, oLevel: str | None = None
605
622
  ) -> None:
606
623
  """Add a text paragraph to the text XML element."""
607
624
  tAttr = {_mkTag("text", "style-name"): self._paraStyle(styleName, oStyle)}
@@ -609,7 +626,7 @@ class ToOdt(Tokenizer):
609
626
  tAttr[_mkTag("text", "outline-level")] = oLevel
610
627
 
611
628
  pTag = "h" if isHead else "p"
612
- xElem = ET.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
629
+ xElem = ET.SubElement(xParent, _mkTag("text", pTag), attrib=tAttr)
613
630
 
614
631
  # It's important to set the initial text field to empty, otherwise
615
632
  # xmlIndent will add a line break if the first subelement is a span.
@@ -627,7 +644,13 @@ class ToOdt(Tokenizer):
627
644
  xFmt = 0x00
628
645
  tFrag = ""
629
646
  fLast = 0
630
- for fPos, fFmt in tFmt:
647
+ xNode = None
648
+ for fPos, fFmt, fData in tFmt:
649
+
650
+ # Add any extra nodes
651
+ if xNode is not None:
652
+ parProc.appendNode(xNode)
653
+ xNode = None
631
654
 
632
655
  # Add the text up to the current fragment
633
656
  if tFrag := tText[fLast:fPos]:
@@ -665,11 +688,18 @@ class ToOdt(Tokenizer):
665
688
  xFmt |= X_SUB
666
689
  elif fFmt == self.FMT_SUB_E:
667
690
  xFmt &= M_SUB
691
+ elif fFmt == self.FMT_FNOTE:
692
+ xNode = self._generateFootnote(fData)
693
+ elif fFmt == self.FMT_STRIP:
694
+ pass
668
695
  else:
669
696
  pErr += 1
670
697
 
671
698
  fLast = fPos
672
699
 
700
+ if xNode is not None:
701
+ parProc.appendNode(xNode)
702
+
673
703
  if tFrag := tText[fLast:]:
674
704
  if xFmt == 0x00:
675
705
  parProc.appendText(tFrag)
@@ -715,7 +745,7 @@ class ToOdt(Tokenizer):
715
745
 
716
746
  style = ODTTextStyle(f"T{len(self._autoText)+1:d}")
717
747
  if hFmt & X_BLD:
718
- style.setFontWeight("bold")
748
+ style.setFontWeight(self._fontBold)
719
749
  if hFmt & X_ITA:
720
750
  style.setFontStyle("italic")
721
751
  if hFmt & X_DEL:
@@ -735,9 +765,25 @@ class ToOdt(Tokenizer):
735
765
 
736
766
  return style.name
737
767
 
768
+ def _generateFootnote(self, key: str) -> ET.Element | None:
769
+ """Generate a footnote XML object."""
770
+ if content := self._footnotes.get(key):
771
+ self._nNote += 1
772
+ nStyle = ODTParagraphStyle("New")
773
+ xNote = ET.Element(_mkTag("text", "note"), attrib={
774
+ _mkTag("text", "id"): f"ftn{self._nNote}",
775
+ _mkTag("text", "note-class"): "footnote",
776
+ })
777
+ xCite = ET.SubElement(xNote, _mkTag("text", "note-citation"))
778
+ xCite.text = str(self._nNote)
779
+ xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
780
+ self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
781
+ return xNote
782
+ return None
783
+
738
784
  def _emToCm(self, value: float) -> str:
739
785
  """Converts an em value to centimetres."""
740
- return f"{value*2.54/72*self._textSize:.3f}cm"
786
+ return f"{value*2.54/72*self._fontSize:.3f}cm"
741
787
 
742
788
  ##
743
789
  # Style Elements
@@ -757,7 +803,6 @@ class ToOdt(Tokenizer):
757
803
  _mkTag("fo", "margin-bottom"): self._mDocBtm,
758
804
  _mkTag("fo", "margin-left"): self._mDocLeft,
759
805
  _mkTag("fo", "margin-right"): self._mDocRight,
760
- _mkTag("fo", "print-orientation"): "portrait",
761
806
  })
762
807
 
763
808
  xHead = ET.SubElement(xPage, _mkTag("style", "header-style"))
@@ -782,8 +827,10 @@ class ToOdt(Tokenizer):
782
827
  _mkTag("style", "writing-mode"): "page",
783
828
  })
784
829
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
785
- _mkTag("style", "font-name"): self._textFont,
830
+ _mkTag("style", "font-name"): self._fontFamily,
786
831
  _mkTag("fo", "font-family"): self._fontFamily,
832
+ _mkTag("fo", "font-weight"): self._fontWeight,
833
+ _mkTag("fo", "font-style"): self._fontStyle,
787
834
  _mkTag("fo", "font-size"): self._fSizeText,
788
835
  _mkTag("fo", "language"): self._dLanguage,
789
836
  _mkTag("fo", "country"): self._dCountry,
@@ -796,8 +843,10 @@ class ToOdt(Tokenizer):
796
843
  _mkTag("style", "class"): "text",
797
844
  })
798
845
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
799
- _mkTag("style", "font-name"): self._textFont,
846
+ _mkTag("style", "font-name"): self._fontFamily,
800
847
  _mkTag("fo", "font-family"): self._fontFamily,
848
+ _mkTag("fo", "font-weight"): self._fontWeight,
849
+ _mkTag("fo", "font-style"): self._fontStyle,
801
850
  _mkTag("fo", "font-size"): self._fSizeText,
802
851
  })
803
852
 
@@ -806,7 +855,7 @@ class ToOdt(Tokenizer):
806
855
  _mkTag("style", "name"): "Heading",
807
856
  _mkTag("style", "family"): "paragraph",
808
857
  _mkTag("style", "parent-style-name"): "Standard",
809
- _mkTag("style", "next-style-name"): "Text_20_body",
858
+ _mkTag("style", "next-style-name"): S_TEXT,
810
859
  _mkTag("style", "class"): "text",
811
860
  })
812
861
  ET.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib={
@@ -815,14 +864,16 @@ class ToOdt(Tokenizer):
815
864
  _mkTag("fo", "keep-with-next"): "always",
816
865
  })
817
866
  ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
818
- _mkTag("style", "font-name"): self._textFont,
867
+ _mkTag("style", "font-name"): self._fontFamily,
819
868
  _mkTag("fo", "font-family"): self._fontFamily,
869
+ _mkTag("fo", "font-weight"): self._fontWeight,
870
+ _mkTag("fo", "font-style"): self._fontStyle,
820
871
  _mkTag("fo", "font-size"): self._fSizeHead,
821
872
  })
822
873
 
823
874
  # Add Header and Footer Styles
824
875
  ET.SubElement(self._xStyl, _mkTag("style", "style"), attrib={
825
- _mkTag("style", "name"): "Header_20_and_20_Footer",
876
+ _mkTag("style", "name"): S_HNF,
826
877
  _mkTag("style", "display-name"): "Header and Footer",
827
878
  _mkTag("style", "family"): "paragraph",
828
879
  _mkTag("style", "parent-style-name"): "Standard",
@@ -834,7 +885,7 @@ class ToOdt(Tokenizer):
834
885
  def _useableStyles(self) -> None:
835
886
  """Set the usable styles."""
836
887
  # Add Text Body Style
837
- style = ODTParagraphStyle("Text_20_body")
888
+ style = ODTParagraphStyle(S_TEXT)
838
889
  style.setDisplayName("Text body")
839
890
  style.setParentStyleName("Standard")
840
891
  style.setClass("text")
@@ -842,136 +893,139 @@ class ToOdt(Tokenizer):
842
893
  style.setMarginBottom(self._mBotText)
843
894
  style.setLineHeight(self._fLineHeight)
844
895
  style.setTextAlign(self._textAlign)
845
- style.setFontName(self._textFont)
896
+ style.setFontName(self._fontFamily)
846
897
  style.setFontFamily(self._fontFamily)
847
898
  style.setFontSize(self._fSizeText)
899
+ style.setFontWeight(self._fontWeight)
848
900
  style.packXML(self._xStyl)
849
901
  self._mainPara[style.name] = style
850
902
 
851
903
  # Add First Line Indent Style
852
- style = ODTParagraphStyle("First_20_line_20_indent")
904
+ style = ODTParagraphStyle(S_FIND)
853
905
  style.setDisplayName("First line indent")
854
- style.setParentStyleName("Text_20_body")
906
+ style.setParentStyleName(S_TEXT)
855
907
  style.setClass("text")
856
908
  style.setTextIndent(self._fTextIndent)
857
909
  style.packXML(self._xStyl)
858
910
  self._mainPara[style.name] = style
859
911
 
860
912
  # Add Text Meta Style
861
- style = ODTParagraphStyle("Text_20_Meta")
913
+ style = ODTParagraphStyle(S_META)
862
914
  style.setDisplayName("Text Meta")
863
915
  style.setParentStyleName("Standard")
864
916
  style.setClass("text")
865
917
  style.setMarginTop(self._mTopMeta)
866
918
  style.setMarginBottom(self._mBotMeta)
867
919
  style.setLineHeight(self._fLineHeight)
868
- style.setFontName(self._textFont)
920
+ style.setFontName(self._fontFamily)
869
921
  style.setFontFamily(self._fontFamily)
870
922
  style.setFontSize(self._fSizeText)
923
+ style.setFontWeight(self._fontWeight)
871
924
  style.setColour(self._colMetaTx)
872
925
  style.setOpacity(self._opaMetaTx)
873
926
  style.packXML(self._xStyl)
874
927
  self._mainPara[style.name] = style
875
928
 
876
929
  # Add Title Style
877
- style = ODTParagraphStyle("Title")
930
+ style = ODTParagraphStyle(S_TITLE)
878
931
  style.setDisplayName("Title")
879
932
  style.setParentStyleName("Heading")
880
- style.setNextStyleName("Text_20_body")
933
+ style.setNextStyleName(S_TEXT)
881
934
  style.setClass("chapter")
882
935
  style.setMarginTop(self._mTopTitle)
883
936
  style.setMarginBottom(self._mBotTitle)
884
937
  style.setTextAlign("center")
885
- style.setFontName(self._textFont)
938
+ style.setFontName(self._fontFamily)
886
939
  style.setFontFamily(self._fontFamily)
887
940
  style.setFontSize(self._fSizeTitle)
888
- style.setFontWeight("bold")
941
+ style.setFontWeight(self._fontBold)
889
942
  style.packXML(self._xStyl)
890
943
  self._mainPara[style.name] = style
891
944
 
892
945
  # Add Separator Style
893
- style = ODTParagraphStyle("Separator")
946
+ style = ODTParagraphStyle(S_SEP)
894
947
  style.setDisplayName("Separator")
895
948
  style.setParentStyleName("Standard")
896
- style.setNextStyleName("Text_20_body")
949
+ style.setNextStyleName(S_TEXT)
897
950
  style.setClass("text")
898
- style.setMarginTop(self._mTopText)
899
- style.setMarginBottom(self._mBotText)
951
+ style.setMarginTop(self._mTopSep)
952
+ style.setMarginBottom(self._mBotSep)
900
953
  style.setLineHeight(self._fLineHeight)
901
954
  style.setTextAlign("center")
902
- style.setFontName(self._textFont)
955
+ style.setFontName(self._fontFamily)
903
956
  style.setFontFamily(self._fontFamily)
904
957
  style.setFontSize(self._fSizeText)
958
+ style.setFontWeight(self._fontWeight)
905
959
  style.packXML(self._xStyl)
906
960
  self._mainPara[style.name] = style
907
961
 
908
962
  # Add Heading 1 Style
909
- style = ODTParagraphStyle("Heading_20_1")
963
+ style = ODTParagraphStyle(S_HEAD1)
910
964
  style.setDisplayName("Heading 1")
911
965
  style.setParentStyleName("Heading")
912
- style.setNextStyleName("Text_20_body")
966
+ style.setNextStyleName(S_TEXT)
913
967
  style.setOutlineLevel("1")
914
968
  style.setClass("text")
915
969
  style.setMarginTop(self._mTopHead1)
916
970
  style.setMarginBottom(self._mBotHead1)
917
- style.setFontName(self._textFont)
971
+ style.setFontName(self._fontFamily)
918
972
  style.setFontFamily(self._fontFamily)
919
973
  style.setFontSize(self._fSizeHead1)
920
- style.setFontWeight("bold")
974
+ style.setFontWeight(self._fontBold)
921
975
  style.setColour(self._colHead12)
922
976
  style.setOpacity(self._opaHead12)
923
977
  style.packXML(self._xStyl)
924
978
  self._mainPara[style.name] = style
925
979
 
926
980
  # Add Heading 2 Style
927
- style = ODTParagraphStyle("Heading_20_2")
981
+ style = ODTParagraphStyle(S_HEAD2)
928
982
  style.setDisplayName("Heading 2")
929
983
  style.setParentStyleName("Heading")
930
- style.setNextStyleName("Text_20_body")
984
+ style.setNextStyleName(S_TEXT)
931
985
  style.setOutlineLevel("2")
932
986
  style.setClass("text")
933
987
  style.setMarginTop(self._mTopHead2)
934
988
  style.setMarginBottom(self._mBotHead2)
935
- style.setFontName(self._textFont)
989
+ style.setFontName(self._fontFamily)
936
990
  style.setFontFamily(self._fontFamily)
937
991
  style.setFontSize(self._fSizeHead2)
938
- style.setFontWeight("bold")
992
+ style.setFontWeight(self._fontBold)
939
993
  style.setColour(self._colHead12)
940
994
  style.setOpacity(self._opaHead12)
941
995
  style.packXML(self._xStyl)
942
996
  self._mainPara[style.name] = style
943
997
 
944
998
  # Add Heading 3 Style
945
- style = ODTParagraphStyle("Heading_20_3")
999
+ style = ODTParagraphStyle(S_HEAD3)
946
1000
  style.setDisplayName("Heading 3")
947
1001
  style.setParentStyleName("Heading")
948
- style.setNextStyleName("Text_20_body")
1002
+ style.setNextStyleName(S_TEXT)
949
1003
  style.setOutlineLevel("3")
950
1004
  style.setClass("text")
951
1005
  style.setMarginTop(self._mTopHead3)
952
1006
  style.setMarginBottom(self._mBotHead3)
953
- style.setFontName(self._textFont)
1007
+ style.setFontName(self._fontFamily)
954
1008
  style.setFontFamily(self._fontFamily)
955
1009
  style.setFontSize(self._fSizeHead3)
956
- style.setFontWeight("bold")
1010
+ style.setFontWeight(self._fontBold)
957
1011
  style.setColour(self._colHead34)
958
1012
  style.setOpacity(self._opaHead34)
959
1013
  style.packXML(self._xStyl)
960
1014
  self._mainPara[style.name] = style
961
1015
 
962
1016
  # Add Heading 4 Style
963
- style = ODTParagraphStyle("Heading_20_4")
1017
+ style = ODTParagraphStyle(S_HEAD4)
964
1018
  style.setDisplayName("Heading 4")
965
1019
  style.setParentStyleName("Heading")
966
- style.setNextStyleName("Text_20_body")
1020
+ style.setNextStyleName(S_TEXT)
967
1021
  style.setOutlineLevel("4")
968
1022
  style.setClass("text")
969
1023
  style.setMarginTop(self._mTopHead4)
970
1024
  style.setMarginBottom(self._mBotHead4)
971
- style.setFontName(self._textFont)
1025
+ style.setFontName(self._fontFamily)
972
1026
  style.setFontFamily(self._fontFamily)
973
1027
  style.setFontSize(self._fSizeHead4)
974
- style.setFontWeight("bold")
1028
+ style.setFontWeight(self._fontBold)
975
1029
  style.setColour(self._colHead34)
976
1030
  style.setOpacity(self._opaHead34)
977
1031
  style.packXML(self._xStyl)
@@ -980,11 +1034,23 @@ class ToOdt(Tokenizer):
980
1034
  # Add Header Style
981
1035
  style = ODTParagraphStyle("Header")
982
1036
  style.setDisplayName("Header")
983
- style.setParentStyleName("Header_20_and_20_Footer")
1037
+ style.setParentStyleName(S_HNF)
984
1038
  style.setTextAlign("right")
985
1039
  style.packXML(self._xStyl)
986
1040
  self._mainPara[style.name] = style
987
1041
 
1042
+ # Add Footnote Style
1043
+ style = ODTParagraphStyle("Footnote")
1044
+ style.setDisplayName("Footnote")
1045
+ style.setParentStyleName("Standard")
1046
+ style.setClass("extra")
1047
+ style.setMarginLeft(self._mLeftFoot)
1048
+ style.setMarginBottom(self._mBotFoot)
1049
+ style.setTextIndent("-"+self._mLeftFoot)
1050
+ style.setFontSize(self._fSizeFoot)
1051
+ style.packXML(self._xStyl)
1052
+ self._mainPara[style.name] = style
1053
+
988
1054
  return
989
1055
 
990
1056
  def _writeHeader(self) -> None:
@@ -1026,12 +1092,9 @@ class ToOdt(Tokenizer):
1026
1092
 
1027
1093
  return
1028
1094
 
1029
- # END Class ToOdt
1030
-
1031
1095
 
1032
- # =============================================================================================== #
1033
- # Auto-Style Classes
1034
- # =============================================================================================== #
1096
+ # Auto-Style Classes
1097
+ # ==================
1035
1098
 
1036
1099
  class ODTParagraphStyle:
1037
1100
  """Wrapper class for the paragraph style setting used by the
@@ -1041,8 +1104,8 @@ class ODTParagraphStyle:
1041
1104
  VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
1042
1105
  VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
1043
1106
  VALID_LEVEL = ["1", "2", "3", "4"]
1044
- VALID_CLASS = ["text", "chapter"]
1045
- VALID_WEIGHT = ["normal", "inherit", "bold"]
1107
+ VALID_CLASS = ["text", "chapter", "extra"]
1108
+ VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM
1046
1109
 
1047
1110
  def __init__(self, name: str) -> None:
1048
1111
 
@@ -1279,16 +1342,14 @@ class ODTParagraphStyle:
1279
1342
 
1280
1343
  return
1281
1344
 
1282
- # END Class ODTParagraphStyle
1283
-
1284
1345
 
1285
1346
  class ODTTextStyle:
1286
1347
  """Wrapper class for the text style setting used by the exporter.
1287
1348
  Only the used settings are exposed here to keep the class minimal
1288
1349
  and fast.
1289
1350
  """
1290
- VALID_WEIGHT = ["normal", "inherit", "bold"]
1291
- VALID_STYLE = ["normal", "inherit", "italic"]
1351
+ VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM
1352
+ VALID_STYLE = ["normal", "italic", "oblique"]
1292
1353
  VALID_POS = ["super", "sub"]
1293
1354
  VALID_LSTYLE = ["none", "solid"]
1294
1355
  VALID_LTYPE = ["single", "double"]
@@ -1404,12 +1465,9 @@ class ODTTextStyle:
1404
1465
  ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
1405
1466
  return
1406
1467
 
1407
- # END Class ODTTextStyle
1408
-
1409
1468
 
1410
- # =============================================================================================== #
1411
- # XML Complex Element Helper Class
1412
- # =============================================================================================== #
1469
+ # XML Complex Element Helper Class
1470
+ # ================================
1413
1471
 
1414
1472
  X_ROOT_TEXT = 0
1415
1473
  X_ROOT_TAIL = 1
@@ -1464,7 +1522,6 @@ class XMLParagraph:
1464
1522
  if c == " ":
1465
1523
  nSpaces += 1
1466
1524
  continue
1467
-
1468
1525
  elif nSpaces > 0:
1469
1526
  self._processSpaces(nSpaces)
1470
1527
  nSpaces = 0
@@ -1475,26 +1532,22 @@ class XMLParagraph:
1475
1532
  self._xTail.tail = ""
1476
1533
  self._nState = X_ROOT_TAIL
1477
1534
  self._chrPos += 1
1478
-
1479
1535
  elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
1480
1536
  self._xSing = ET.SubElement(self._xTail, TAG_BR)
1481
1537
  self._xSing.tail = ""
1482
1538
  self._nState = X_SPAN_SING
1483
1539
  self._chrPos += 1
1484
-
1485
1540
  elif c == "\t":
1486
1541
  if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
1487
1542
  self._xTail = ET.SubElement(self._xRoot, TAG_TAB)
1488
1543
  self._xTail.tail = ""
1489
1544
  self._nState = X_ROOT_TAIL
1490
1545
  self._chrPos += 1
1491
-
1492
1546
  elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
1493
1547
  self._xSing = ET.SubElement(self._xTail, TAG_TAB)
1494
1548
  self._xSing.tail = ""
1495
1549
  self._chrPos += 1
1496
1550
  self._nState = X_SPAN_SING
1497
-
1498
1551
  else:
1499
1552
  if self._nState == X_ROOT_TEXT:
1500
1553
  self._xRoot.text = (self._xRoot.text or "") + c
@@ -1529,6 +1582,19 @@ class XMLParagraph:
1529
1582
  self._nState = X_ROOT_TAIL
1530
1583
  return
1531
1584
 
1585
+ def appendNode(self, xNode: ET.Element | None) -> None:
1586
+ """Append an XML node to the paragraph. We only check for the
1587
+ X_ROOT_TEXT and X_ROOT_TAIL states. X_SPAN_TEXT is not possible
1588
+ at all, and X_SPAN_SING only happens internally in an appendSpan
1589
+ call, returning us to an X_ROOT_TAIL state.
1590
+ """
1591
+ if xNode is not None and self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
1592
+ self._xRoot.append(xNode)
1593
+ self._xTail = xNode
1594
+ self._xTail.tail = ""
1595
+ self._nState = X_ROOT_TAIL
1596
+ return
1597
+
1532
1598
  def checkError(self) -> tuple[int, str]:
1533
1599
  """Check that the number of characters written matches the
1534
1600
  number of characters received.
@@ -1600,5 +1666,3 @@ class XMLParagraph:
1600
1666
  self._chrPos += nSpaces - 1
1601
1667
 
1602
1668
  return
1603
-
1604
- # END Class XMLParagraph