novelWriter 2.1.1__py3-none-any.whl → 2.2rc1__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.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
  3. novelwriter/__init__.py +6 -24
  4. novelwriter/assets/i18n/project_de_DE.json +10 -0
  5. novelwriter/assets/i18n/project_en_GB.json +11 -0
  6. novelwriter/assets/i18n/project_en_US.json +10 -0
  7. novelwriter/assets/i18n/project_ja_JP.json +11 -1
  8. novelwriter/assets/i18n/project_nb_NO.json +10 -0
  9. novelwriter/assets/i18n/project_nn_NO.json +10 -0
  10. novelwriter/assets/icons/novelwriter.ico +0 -0
  11. novelwriter/assets/icons/novelwriter.svg +8 -183
  12. novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
  13. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  14. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
  21. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
  22. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
  25. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
  26. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/icons.conf +17 -2
  29. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  30. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  33. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
  35. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
  37. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
  40. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
  42. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
  44. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  45. novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
  46. novelwriter/assets/manual.pdf +0 -0
  47. novelwriter/assets/sample.zip +0 -0
  48. novelwriter/assets/syntax/default_dark.conf +1 -0
  49. novelwriter/assets/syntax/default_light.conf +1 -0
  50. novelwriter/assets/syntax/grey_dark.conf +1 -0
  51. novelwriter/assets/syntax/grey_light.conf +1 -0
  52. novelwriter/assets/syntax/light_owl.conf +1 -0
  53. novelwriter/assets/syntax/night_owl.conf +1 -0
  54. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  55. novelwriter/assets/syntax/solarized_light.conf +1 -0
  56. novelwriter/assets/syntax/tomorrow.conf +1 -0
  57. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  58. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  59. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  60. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  61. novelwriter/assets/text/credits_en.htm +7 -0
  62. novelwriter/assets/text/release_notes.htm +7 -37
  63. novelwriter/common.py +22 -1
  64. novelwriter/config.py +27 -42
  65. novelwriter/constants.py +45 -7
  66. novelwriter/core/buildsettings.py +40 -24
  67. novelwriter/core/coretools.py +8 -1
  68. novelwriter/core/docbuild.py +2 -6
  69. novelwriter/core/index.py +264 -175
  70. novelwriter/core/options.py +8 -3
  71. novelwriter/core/project.py +2 -2
  72. novelwriter/core/projectdata.py +3 -3
  73. novelwriter/core/tohtml.py +60 -59
  74. novelwriter/core/tokenizer.py +110 -70
  75. novelwriter/core/tomd.py +51 -38
  76. novelwriter/core/toodt.py +184 -147
  77. novelwriter/dialogs/preferences.py +75 -106
  78. novelwriter/dialogs/projsettings.py +101 -110
  79. novelwriter/dialogs/updates.py +25 -14
  80. novelwriter/enum.py +28 -3
  81. novelwriter/extensions/novelselector.py +1 -1
  82. novelwriter/gui/doceditor.py +1345 -1235
  83. novelwriter/gui/dochighlight.py +98 -62
  84. novelwriter/gui/docviewer.py +151 -340
  85. novelwriter/gui/docviewerpanel.py +457 -0
  86. novelwriter/gui/editordocument.py +126 -0
  87. novelwriter/gui/mainmenu.py +350 -300
  88. novelwriter/gui/noveltree.py +101 -125
  89. novelwriter/gui/outline.py +154 -171
  90. novelwriter/gui/projtree.py +480 -380
  91. novelwriter/gui/sidebar.py +106 -75
  92. novelwriter/gui/statusbar.py +1 -1
  93. novelwriter/gui/theme.py +114 -75
  94. novelwriter/guimain.py +353 -254
  95. novelwriter/shared.py +36 -3
  96. novelwriter/tools/dictionaries.py +268 -0
  97. novelwriter/tools/manusbuild.py +17 -6
  98. novelwriter/tools/manuscript.py +11 -3
  99. novelwriter/tools/manussettings.py +0 -14
  100. novelwriter/tools/projwizard.py +16 -2
  101. novelwriter/tools/writingstats.py +1 -1
  102. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  103. novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
  104. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  105. novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
  106. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  109. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
novelwriter/core/toodt.py CHANGED
@@ -27,10 +27,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
27
27
  from __future__ import annotations
28
28
 
29
29
  import logging
30
- from pathlib import Path
31
30
  import xml.etree.ElementTree as ET
32
31
 
32
+ from typing import Sequence
33
33
  from hashlib import sha256
34
+ from pathlib import Path
34
35
  from zipfile import ZipFile
35
36
  from datetime import datetime
36
37
 
@@ -82,11 +83,17 @@ TAG_STNM = _mkTag("text", "style-name")
82
83
  X_BLD = 0x01 # Bold format
83
84
  X_ITA = 0x02 # Italic format
84
85
  X_DEL = 0x04 # Strikethrough format
86
+ X_UND = 0x08 # Underline format
87
+ X_SUP = 0x10 # Superscript
88
+ X_SUB = 0x20 # Subscript
85
89
 
86
90
  # Formatting Masks
87
91
  M_BLD = ~X_BLD
88
92
  M_ITA = ~X_ITA
89
93
  M_DEL = ~X_DEL
94
+ M_UND = ~X_UND
95
+ M_SUP = ~X_SUP
96
+ M_SUB = ~X_SUB
90
97
 
91
98
 
92
99
  class ToOdt(Tokenizer):
@@ -188,7 +195,7 @@ class ToOdt(Tokenizer):
188
195
  # Setters
189
196
  ##
190
197
 
191
- def setLanguage(self, language: str) -> None:
198
+ def setLanguage(self, language: str | None) -> None:
192
199
  """Set language for the document."""
193
200
  if language:
194
201
  langBits = language.split("_")
@@ -385,18 +392,9 @@ class ToOdt(Tokenizer):
385
392
  """Convert the list of text tokens into XML elements."""
386
393
  self._result = "" # Not used, but cleared just in case
387
394
 
388
- odtTags = {
389
- self.FMT_B_B: "_B", # Bold open format
390
- self.FMT_B_E: "b_", # Bold close format
391
- self.FMT_I_B: "I", # Italic open format
392
- self.FMT_I_E: "i", # Italic close format
393
- self.FMT_D_B: "_S", # Strikethrough open format
394
- self.FMT_D_E: "s_", # Strikethrough close format
395
- }
396
-
397
- thisPar = []
398
- thisFmt = []
399
- parStyle = None
395
+ pFmt = []
396
+ pText = []
397
+ pStyle = None
400
398
  for tType, _, tText, tFormat, tStyle in self._tokens:
401
399
 
402
400
  # Styles
@@ -429,20 +427,22 @@ class ToOdt(Tokenizer):
429
427
 
430
428
  # Process Text Types
431
429
  if tType == self.T_EMPTY:
432
- if len(thisPar) > 1 and parStyle is not None:
430
+ if len(pText) > 1 and pStyle is not None:
433
431
  if self._doJustify:
434
- parStyle.setTextAlign("left")
432
+ pStyle.setTextAlign("left")
435
433
 
436
- if len(thisPar) > 0 and parStyle is not None:
437
- tTemp = "\n".join(thisPar)
438
- fTemp = " ".join(thisFmt)
439
- tTxt = tTemp.rstrip()
440
- tFmt = fTemp[:len(tTxt)]
441
- self._addTextPar("Text_20_body", parStyle, tTxt, tFmt=tFmt)
434
+ if len(pText) > 0 and pStyle is not None:
435
+ tTxt = ""
436
+ tFmt = []
437
+ for nText, nFmt in zip(pText, pFmt):
438
+ tLen = len(tTxt)
439
+ tTxt += f"{nText}\n"
440
+ tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
441
+ self._addTextPar("Text_20_body", pStyle, tTxt.rstrip(), tFmt=tFmt)
442
442
 
443
- thisPar = []
444
- thisFmt = []
445
- parStyle = None
443
+ pFmt = []
444
+ pText = []
445
+ pStyle = None
446
446
 
447
447
  elif tType == self.T_TITLE:
448
448
  tHead = tText.replace(nwHeadFmt.BR, "\n")
@@ -475,20 +475,17 @@ class ToOdt(Tokenizer):
475
475
  self._addTextPar("Separator", oStyle, "")
476
476
 
477
477
  elif tType == self.T_TEXT:
478
- if parStyle is None:
479
- parStyle = oStyle
480
-
481
- tFmt = " "*len(tText)
482
- for xPos, xLen, xFmt in tFormat:
483
- tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
484
-
485
- tTxt = tText.rstrip()
486
- tFmt = tFmt[:len(tTxt)]
487
- thisPar.append(tTxt)
488
- thisFmt.append(tFmt)
478
+ if pStyle is None:
479
+ pStyle = oStyle
480
+ pText.append(tText)
481
+ pFmt.append(tFormat)
489
482
 
490
483
  elif tType == self.T_SYNOPSIS and self._doSynopsis:
491
- tTemp, fTemp = self._formatSynopsis(tText)
484
+ tTemp, fTemp = self._formatSynopsis(tText, True)
485
+ self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
486
+
487
+ elif tType == self.T_SHORT and self._doSynopsis:
488
+ tTemp, fTemp = self._formatSynopsis(tText, False)
492
489
  self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
493
490
 
494
491
  elif tType == self.T_COMMENT and self._doComments:
@@ -501,8 +498,8 @@ class ToOdt(Tokenizer):
501
498
 
502
499
  return
503
500
 
504
- def closeDocument(self):
505
- """Return the serialised XML document"""
501
+ def closeDocument(self) -> None:
502
+ """Pack the styles of the XML document."""
506
503
  # Build the auto-generated styles
507
504
  for styleName, styleObj in self._autoPara.values():
508
505
  styleObj.packXML(self._xAuto, styleName)
@@ -510,7 +507,7 @@ class ToOdt(Tokenizer):
510
507
  styleObj.packXML(self._xAuto, styleName)
511
508
  return
512
509
 
513
- def saveFlatXML(self, path: str | Path):
510
+ def saveFlatXML(self, path: str | Path) -> None:
514
511
  """Save the data to an .fodt file."""
515
512
  with open(path, mode="wb") as fObj:
516
513
  xml = ET.ElementTree(self._dFlat)
@@ -519,7 +516,7 @@ class ToOdt(Tokenizer):
519
516
  logger.info("Wrote file: %s", path)
520
517
  return
521
518
 
522
- def saveOpenDocText(self, path: str | Path):
519
+ def saveOpenDocText(self, path: str | Path) -> None:
523
520
  """Save the data to an .odt file."""
524
521
  mMani = _mkTag("manifest", "manifest")
525
522
  mVers = _mkTag("manifest", "version")
@@ -559,46 +556,42 @@ class ToOdt(Tokenizer):
559
556
  # Internal Functions
560
557
  ##
561
558
 
562
- def _formatSynopsis(self, text: str) -> tuple[str, str]:
559
+ def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
563
560
  """Apply formatting to synopsis lines."""
564
- sSynop = self._localLookup("Synopsis")
565
- rTxt = "**{0}:** {1}".format(sSynop, text)
566
- rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(text))
561
+ if synopsis:
562
+ name = self._localLookup("Synopsis")
563
+ else:
564
+ name = self._localLookup("Short Description")
565
+ rTxt = f"{name}: {text}"
566
+ rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
567
567
  return rTxt, rFmt
568
568
 
569
- def _formatComments(self, text: str) -> tuple[str, str]:
569
+ def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
570
570
  """Apply formatting to comments."""
571
- sComm = self._localLookup("Comment")
572
- rTxt = "**{0}:** {1}".format(sComm, text)
573
- rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(text))
571
+ name = self._localLookup("Comment")
572
+ rTxt = f"{name}: {text}"
573
+ rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
574
574
  return rTxt, rFmt
575
575
 
576
- def _formatKeywords(self, text: str) -> tuple[str, str]:
576
+ def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
577
577
  """Apply formatting to keywords."""
578
578
  valid, bits, _ = self._project.index.scanThis("@"+text)
579
- if not valid or not bits:
580
- return "", ""
581
-
582
- rTxt = ""
583
- rFmt = ""
584
- if bits[0] in nwLabels.KEY_NAME:
585
- text = nwLabels.KEY_NAME[bits[0]]
586
- rTxt += "**{0}:** ".format(text)
587
- rFmt += "_B{0} b_ ".format(" "*len(text))
588
- if len(bits) > 1:
589
- if bits[0] == nwKeyWords.TAG_KEY:
590
- rTxt += bits[1]
591
- rFmt += " "*len(bits[1])
592
- else:
593
- tTags = ", ".join(bits[1:])
594
- rTxt += tTags
595
- rFmt += (" "*len(tTags))
579
+ if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
580
+ return "", []
581
+
582
+ rTxt = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
583
+ rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
584
+ if len(bits) > 1:
585
+ if bits[0] == nwKeyWords.TAG_KEY:
586
+ rTxt += bits[1]
587
+ else:
588
+ rTxt += ", ".join(bits[1:])
596
589
 
597
590
  return rTxt, rFmt
598
591
 
599
592
  def _addTextPar(
600
- self, styleName: str, oStyle: ODTParagraphStyle, tText: str, tFmt: str = "",
601
- isHead: bool = False, oLevel: str | None = None
593
+ self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
594
+ tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
602
595
  ) -> None:
603
596
  """Add a text paragraph to the text XML element."""
604
597
  tAttr = {}
@@ -616,58 +609,59 @@ class ToOdt(Tokenizer):
616
609
  if not tText:
617
610
  return
618
611
 
619
- ##
620
- # Process Formatting
621
- ##
622
-
623
- if len(tText) != len(tFmt):
624
- # Generate an empty format if there isn't any or it doesn't match
625
- tFmt = " "*len(tText)
626
-
627
- # The formatting loop
628
- tTemp = ""
629
- xFmt = 0x00
630
- pFmt = 0x00
631
- pErr = 0
612
+ # Loop Over Fragments
613
+ # ===================
632
614
 
633
615
  parProc = XMLParagraph(xElem)
634
616
 
635
- for i, c in enumerate(tText):
617
+ pErr = 0
618
+ xFmt = 0x00
619
+ tFrag = ""
620
+ fLast = 0
621
+ for fPos, fFmt in tFmt:
622
+
623
+ # Add the text up to the current fragment
624
+ if tFrag := tText[fLast:fPos]:
625
+ if xFmt == 0x00:
626
+ parProc.appendText(tFrag)
627
+ else:
628
+ parProc.appendSpan(tFrag, self._textStyle(xFmt))
636
629
 
637
- if tFmt[i] == " ":
638
- tTemp += c
639
- elif tFmt[i] == "_":
640
- continue
641
- elif tFmt[i] == "B":
630
+ # Calculate the change of format
631
+ if fFmt == self.FMT_B_B:
642
632
  xFmt |= X_BLD
643
- elif tFmt[i] == "b":
633
+ elif fFmt == self.FMT_B_E:
644
634
  xFmt &= M_BLD
645
- elif tFmt[i] == "I":
635
+ elif fFmt == self.FMT_I_B:
646
636
  xFmt |= X_ITA
647
- elif tFmt[i] == "i":
637
+ elif fFmt == self.FMT_I_E:
648
638
  xFmt &= M_ITA
649
- elif tFmt[i] == "S":
639
+ elif fFmt == self.FMT_D_B:
650
640
  xFmt |= X_DEL
651
- elif tFmt[i] == "s":
641
+ elif fFmt == self.FMT_D_E:
652
642
  xFmt &= M_DEL
643
+ elif fFmt == self.FMT_U_B:
644
+ xFmt |= X_UND
645
+ elif fFmt == self.FMT_U_E:
646
+ xFmt &= M_UND
647
+ elif fFmt == self.FMT_SUP_B:
648
+ xFmt |= X_SUP
649
+ elif fFmt == self.FMT_SUP_E:
650
+ xFmt &= M_SUP
651
+ elif fFmt == self.FMT_SUB_B:
652
+ xFmt |= X_SUB
653
+ elif fFmt == self.FMT_SUB_E:
654
+ xFmt &= M_SUB
653
655
  else:
654
656
  pErr += 1
655
657
 
656
- if xFmt != pFmt:
657
- if pFmt == 0x00:
658
- parProc.appendText(tTemp)
659
- tTemp = ""
660
- else:
661
- parProc.appendSpan(tTemp, self._textStyle(pFmt))
662
- tTemp = ""
663
-
664
- pFmt = xFmt
658
+ fLast = fPos
665
659
 
666
- # Save what remains in the buffer
667
- if pFmt == 0x00:
668
- parProc.appendText(tTemp)
669
- else:
670
- parProc.appendSpan(tTemp, self._textStyle(pFmt))
660
+ if tFrag := tText[fLast:]:
661
+ if xFmt == 0x00:
662
+ parProc.appendText(tFrag)
663
+ else:
664
+ parProc.appendSpan(tFrag, self._textStyle(xFmt))
671
665
 
672
666
  if pErr > 0:
673
667
  self._errData.append("Unknown format tag encountered")
@@ -713,14 +707,22 @@ class ToOdt(Tokenizer):
713
707
  if hFmt & X_DEL:
714
708
  newStyle.setStrikeStyle("solid")
715
709
  newStyle.setStrikeType("single")
710
+ if hFmt & X_UND:
711
+ newStyle.setUnderlineStyle("solid")
712
+ newStyle.setUnderlineWidth("auto")
713
+ newStyle.setUnderlineColour("font-color")
714
+ if hFmt & X_SUP:
715
+ newStyle.setTextPosition("super")
716
+ if hFmt & X_SUB:
717
+ newStyle.setTextPosition("sub")
716
718
 
717
719
  self._autoText[hFmt] = (newName, newStyle)
718
720
 
719
721
  return newName
720
722
 
721
- def _emToCm(self, emVal: float) -> str:
723
+ def _emToCm(self, value: float) -> str:
722
724
  """Converts an em value to centimetres."""
723
- return f"{emVal*2.54/72*self._textSize:.3f}cm"
725
+ return f"{value*2.54/72*self._textSize:.3f}cm"
724
726
 
725
727
  ##
726
728
  # Style Elements
@@ -1193,56 +1195,56 @@ class ODTParagraphStyle:
1193
1195
  # Methods
1194
1196
  ##
1195
1197
 
1196
- def checkNew(self, refStyle: ODTParagraphStyle) -> bool:
1198
+ def checkNew(self, style: ODTParagraphStyle) -> bool:
1197
1199
  """Check if there are new settings in refStyle that differ from
1198
1200
  those in the current object.
1199
1201
  """
1200
- for aName, (_, aVal) in refStyle._mAttr.items():
1201
- if aVal is not None and aVal != self._mAttr[aName][1]:
1202
+ for name, (_, aVal) in style._mAttr.items():
1203
+ if aVal is not None and aVal != self._mAttr[name][1]:
1202
1204
  return True
1203
- for aName, (_, aVal) in refStyle._pAttr.items():
1204
- if aVal is not None and aVal != self._pAttr[aName][1]:
1205
+ for name, (_, aVal) in style._pAttr.items():
1206
+ if aVal is not None and aVal != self._pAttr[name][1]:
1205
1207
  return True
1206
- for aName, (_, aVal) in refStyle._tAttr.items():
1207
- if aVal is not None and aVal != self._tAttr[aName][1]:
1208
+ for name, (_, aVal) in style._tAttr.items():
1209
+ if aVal is not None and aVal != self._tAttr[name][1]:
1208
1210
  return True
1209
1211
  return False
1210
1212
 
1211
1213
  def getID(self) -> str:
1212
1214
  """Generate a unique ID from the settings."""
1213
- theString = (
1215
+ string = (
1214
1216
  f"Paragraph:Main:{str(self._mAttr)}:"
1215
1217
  f"Paragraph:Para:{str(self._pAttr)}:"
1216
1218
  f"Paragraph:Text:{str(self._tAttr)}:"
1217
1219
  )
1218
- return sha256(theString.encode()).hexdigest()
1220
+ return sha256(string.encode()).hexdigest()
1219
1221
 
1220
1222
  def packXML(self, xParent: ET.Element, name: str) -> None:
1221
1223
  """Pack the content into an xml element."""
1222
- theAttr = {}
1223
- theAttr[_mkTag("style", "name")] = name
1224
- theAttr[_mkTag("style", "family")] = "paragraph"
1224
+ attr = {}
1225
+ attr[_mkTag("style", "name")] = name
1226
+ attr[_mkTag("style", "family")] = "paragraph"
1225
1227
  for aName, (aNm, aVal) in self._mAttr.items():
1226
1228
  if aVal is not None:
1227
- theAttr[_mkTag(aNm, aName)] = aVal
1229
+ attr[_mkTag(aNm, aName)] = aVal
1228
1230
 
1229
- xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
1231
+ xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
1230
1232
 
1231
- theAttr = {}
1233
+ attr = {}
1232
1234
  for aName, (aNm, aVal) in self._pAttr.items():
1233
1235
  if aVal is not None:
1234
- theAttr[_mkTag(aNm, aName)] = aVal
1236
+ attr[_mkTag(aNm, aName)] = aVal
1235
1237
 
1236
- if theAttr:
1237
- ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
1238
+ if attr:
1239
+ ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=attr)
1238
1240
 
1239
- theAttr = {}
1241
+ attr = {}
1240
1242
  for aName, (aNm, aVal) in self._tAttr.items():
1241
1243
  if aVal is not None:
1242
- theAttr[_mkTag(aNm, aName)] = aVal
1244
+ attr[_mkTag(aNm, aName)] = aVal
1243
1245
 
1244
- if theAttr:
1245
- ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
1246
+ if attr:
1247
+ ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
1246
1248
 
1247
1249
  return
1248
1250
 
@@ -1256,16 +1258,23 @@ class ODTTextStyle:
1256
1258
  """
1257
1259
  VALID_WEIGHT = ["normal", "inherit", "bold"]
1258
1260
  VALID_STYLE = ["normal", "inherit", "italic"]
1261
+ VALID_POS = ["super", "sub"]
1259
1262
  VALID_LSTYLE = ["none", "solid"]
1260
- VALID_LTYPE = ["none", "single", "double"]
1263
+ VALID_LTYPE = ["single", "double"]
1264
+ VALID_LWIDTH = ["auto"]
1265
+ VALID_LCOL = ["font-color"]
1261
1266
 
1262
1267
  def __init__(self) -> None:
1263
1268
  # Text Attributes
1264
1269
  self._tAttr = {
1265
1270
  "font-weight": ["fo", None],
1266
1271
  "font-style": ["fo", None],
1272
+ "text-position": ["style", None],
1267
1273
  "text-line-through-style": ["style", None],
1268
1274
  "text-line-through-type": ["style", None],
1275
+ "text-underline-style": ["style", None],
1276
+ "text-underline-width": ["style", None],
1277
+ "text-underline-color": ["style", None],
1269
1278
  }
1270
1279
  return
1271
1280
 
@@ -1287,6 +1296,13 @@ class ODTTextStyle:
1287
1296
  self._tAttr["font-style"][1] = None
1288
1297
  return
1289
1298
 
1299
+ def setTextPosition(self, value: str | None) -> None:
1300
+ if value in self.VALID_POS:
1301
+ self._tAttr["text-position"][1] = f"{value} 58%"
1302
+ else:
1303
+ self._tAttr["text-position"][1] = None
1304
+ return
1305
+
1290
1306
  def setStrikeStyle(self, value: str | None) -> None:
1291
1307
  if value in self.VALID_LSTYLE:
1292
1308
  self._tAttr["text-line-through-style"][1] = value
@@ -1301,24 +1317,45 @@ class ODTTextStyle:
1301
1317
  self._tAttr["text-line-through-type"][1] = None
1302
1318
  return
1303
1319
 
1320
+ def setUnderlineStyle(self, value: str | None) -> None:
1321
+ if value in self.VALID_LSTYLE:
1322
+ self._tAttr["text-underline-style"][1] = value
1323
+ else:
1324
+ self._tAttr["text-underline-style"][1] = None
1325
+ return
1326
+
1327
+ def setUnderlineWidth(self, value: str | None) -> None:
1328
+ if value in self.VALID_LWIDTH:
1329
+ self._tAttr["text-underline-width"][1] = value
1330
+ else:
1331
+ self._tAttr["text-underline-width"][1] = None
1332
+ return
1333
+
1334
+ def setUnderlineColour(self, value: str | None) -> None:
1335
+ if value in self.VALID_LCOL:
1336
+ self._tAttr["text-underline-color"][1] = value
1337
+ else:
1338
+ self._tAttr["text-underline-color"][1] = None
1339
+ return
1340
+
1304
1341
  ##
1305
1342
  # Methods
1306
1343
  ##
1307
1344
 
1308
1345
  def packXML(self, xParent: ET.Element, name: str) -> None:
1309
1346
  """Pack the content into an xml element."""
1310
- theAttr = {}
1311
- theAttr[_mkTag("style", "name")] = name
1312
- theAttr[_mkTag("style", "family")] = "text"
1313
- xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
1347
+ attr = {}
1348
+ attr[_mkTag("style", "name")] = name
1349
+ attr[_mkTag("style", "family")] = "text"
1350
+ xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
1314
1351
 
1315
- theAttr = {}
1352
+ attr = {}
1316
1353
  for aName, (aNm, aVal) in self._tAttr.items():
1317
1354
  if aVal is not None:
1318
- theAttr[_mkTag(aNm, aName)] = aVal
1355
+ attr[_mkTag(aNm, aName)] = aVal
1319
1356
 
1320
- if theAttr:
1321
- ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
1357
+ if attr:
1358
+ ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
1322
1359
 
1323
1360
  return
1324
1361
 
@@ -1368,17 +1405,17 @@ class XMLParagraph:
1368
1405
 
1369
1406
  return
1370
1407
 
1371
- def appendText(self, tText: str) -> None:
1408
+ def appendText(self, text: str) -> None:
1372
1409
  """Append text to the XML element. We do this one character at
1373
1410
  the time in order to be able to process line breaks, tabs and
1374
1411
  spaces separately. Multiple spaces are concatenated into a
1375
1412
  single tag, and must therefore be processed separately.
1376
1413
  """
1377
- tText = stripEscape(tText)
1414
+ text = stripEscape(text)
1378
1415
  nSpaces = 0
1379
- self._rawTxt += tText
1416
+ self._rawTxt += text
1380
1417
 
1381
- for c in tText:
1418
+ for c in text:
1382
1419
  if c == " ":
1383
1420
  nSpaces += 1
1384
1421
  continue
@@ -1433,17 +1470,17 @@ class XMLParagraph:
1433
1470
 
1434
1471
  return
1435
1472
 
1436
- def appendSpan(self, tText: str, tFmt: str) -> None:
1473
+ def appendSpan(self, text: str, fmt: str) -> None:
1437
1474
  """Append a text span to the XML element. The span is always
1438
1475
  closed since we do not allow nested spans (like Libre Office).
1439
1476
  Therefore we return to the root element level when we're done
1440
1477
  processing the text of the span.
1441
1478
  """
1442
- self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: tFmt})
1479
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: fmt})
1443
1480
  self._xTail.text = "" # Defaults to None
1444
1481
  self._xTail.tail = "" # Defaults to None
1445
1482
  self._nState = X_SPAN_TEXT
1446
- self.appendText(tText)
1483
+ self.appendText(text)
1447
1484
  self._nState = X_ROOT_TAIL
1448
1485
  return
1449
1486