diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 06912fc5..6ebaeedf 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -174,7 +174,26 @@ class ToHtml(Tokenizer):
for tType, tLine, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
- tText = tText.replace("<", "<").replace(">", ">")
+ if tFormat:
+ # If we have formatting, we must recompute the locations
+ cText = []
+ i = 0
+ for c in tText:
+ if c == "<":
+ cText.append("<")
+ tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
+ i += 4
+ elif c == ">":
+ cText.append(">")
+ tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
+ i += 4
+ else:
+ cText.append(c)
+ i += 1
+ tText = "".join(cText)
+ else:
+ # If we don't have formatting, we can do a plain replace
+ tText = tText.replace("<", "<").replace(">", ">")
# Styles
aStyle = []
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 1aeefb80..590e4c0e 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -36,7 +36,7 @@ from datetime import datetime
from novelwriter import __version__
from novelwriter.common import xmlIndent
-from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels
+from novelwriter.constants import nwHeadFmt, nwLabels, trConst
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer, stripEscape
@@ -391,23 +391,8 @@ class ToOdt(Tokenizer):
"""Convert the list of text tokens into XML elements."""
self._result = "" # Not used, but cleared just in case
- odtTags = {
- self.FMT_B_B: "B", # Bold open format
- self.FMT_B_E: "b", # Bold close format
- self.FMT_I_B: "I", # Italic open format
- self.FMT_I_E: "i", # Italic close format
- self.FMT_D_B: "S", # Strikethrough open format
- self.FMT_D_E: "s", # Strikethrough close format
- self.FMT_U_B: "U", # Underline open format
- self.FMT_U_E: "u", # Underline close format
- self.FMT_SUP_B: "P", # Superscript open format
- self.FMT_SUP_E: "p", # Superscript close format
- self.FMT_SUB_B: "D", # Subscript open format
- self.FMT_SUB_E: "d", # Subscript close format
- }
-
- fmt = []
- para = []
+ pFmt = []
+ pText = []
pStyle = None
for tType, _, tText, tFormat, tStyle in self._tokens:
@@ -441,19 +426,21 @@ class ToOdt(Tokenizer):
# Process Text Types
if tType == self.T_EMPTY:
- if len(para) > 1 and pStyle is not None:
+ if len(pText) > 1 and pStyle is not None:
if self._doJustify:
pStyle.setTextAlign("left")
- if len(para) > 0 and pStyle is not None:
- tTemp = "\n".join(para)
- fTemp = " ".join(fmt)
- tTxt = tTemp.rstrip()
- tFmt = fTemp[:len(tTxt)]
- self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
+ if len(pText) > 0 and pStyle is not None:
+ tTxt = ""
+ tFmt = []
+ for nText, nFmt in zip(pText, pFmt):
+ tLen = len(tTxt)
+ tTxt += f"{nText}\n"
+ tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
+ self._addTextPar("Text_20_body", pStyle, tTxt.rstrip(), tFmt=tFmt)
- fmt = []
- para = []
+ pFmt = []
+ pText = []
pStyle = None
elif tType == self.T_TITLE:
@@ -489,18 +476,8 @@ class ToOdt(Tokenizer):
elif tType == self.T_TEXT:
if pStyle is None:
pStyle = oStyle
-
- tFmt = " "*len(tText)
- for xPos, xLen, xFmt in tFormat:
- if xFmt%2 == 0: # Even number: End
- tFmt = tFmt[:xPos] + odtTags[xFmt].ljust(xLen, "_") + tFmt[xPos+xLen:]
- else: # Odd number: Begin
- tFmt = tFmt[:xPos] + odtTags[xFmt].rjust(xLen, "_") + tFmt[xPos+xLen:]
-
- tTxt = tText.rstrip()
- tFmt = tFmt[:len(tTxt)]
- para.append(tTxt)
- fmt.append(tFmt)
+ pText.append(tText)
+ pFmt.append(tFormat)
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
@@ -574,46 +551,34 @@ class ToOdt(Tokenizer):
# Internal Functions
##
- def _formatSynopsis(self, text: str) -> tuple[str, str]:
+ def _formatSynopsis(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to synopsis lines."""
- sSynop = self._localLookup("Synopsis")
- rTxt = "**{0}:** {1}".format(sSynop, text)
- rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(text))
+ name = self._localLookup("Synopsis")
+ rTxt = f"{name}: {text}"
+ rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt
- def _formatComments(self, text: str) -> tuple[str, str]:
+ def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to comments."""
- sComm = self._localLookup("Comment")
- rTxt = "**{0}:** {1}".format(sComm, text)
- rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(text))
+ name = self._localLookup("Comment")
+ rTxt = f"{name}: {text}"
+ rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt
- def _formatKeywords(self, text: str) -> tuple[str, str]:
+ def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
- if not valid or not bits:
- return "", ""
-
- rTxt = ""
- rFmt = ""
- if bits[0] in nwLabels.KEY_NAME:
- text = nwLabels.KEY_NAME[bits[0]]
- rTxt += "**{0}:** ".format(text)
- rFmt += "_B{0} b_ ".format(" "*len(text))
- if len(bits) > 1:
- if bits[0] == nwKeyWords.TAG_KEY:
- rTxt += bits[1]
- rFmt += " "*len(bits[1])
- else:
- tTags = ", ".join(bits[1:])
- rTxt += tTags
- rFmt += (" "*len(tTags))
-
+ if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
+ return "", []
+ name = trConst(nwLabels.KEY_NAME[bits[0]])
+ tags = ", ".join(bits[1:])
+ rTxt = f"{name}: {tags}"
+ rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt
def _addTextPar(
- self, styleName: str, oStyle: ODTParagraphStyle, tText: str, tFmt: str = "",
- isHead: bool = False, oLevel: str | None = None
+ self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
+ tFmt: list[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
) -> None:
"""Add a text paragraph to the text XML element."""
tAttr = {}
@@ -631,70 +596,59 @@ class ToOdt(Tokenizer):
if not tText:
return
- ##
- # Process Formatting
- ##
-
- if len(tText) != len(tFmt):
- # Generate an empty format if there isn't any or it doesn't match
- tFmt = " "*len(tText)
-
- # The formatting loop
- tTemp = ""
- xFmt = 0x00
- pFmt = 0x00
- pErr = 0
+ # Loop Over Fragments
+ # ===================
parProc = XMLParagraph(xElem)
- for i, c in enumerate(tText):
+ pErr = 0
+ xFmt = 0x00
+ tFrag = ""
+ fLast = 0
+ for fPos, fFmt in tFmt:
- if tFmt[i] == " ":
- tTemp += c
- elif tFmt[i] == "_":
- continue
- elif tFmt[i] == "B":
+ # Add the text up to the current fragment
+ if tFrag := tText[fLast:fPos]:
+ if xFmt == 0x00:
+ parProc.appendText(tFrag)
+ else:
+ parProc.appendSpan(tFrag, self._textStyle(xFmt))
+
+ # Calculate the change of format
+ if fFmt == self.FMT_B_B:
xFmt |= X_BLD
- elif tFmt[i] == "b":
+ elif fFmt == self.FMT_B_E:
xFmt &= M_BLD
- elif tFmt[i] == "I":
+ elif fFmt == self.FMT_I_B:
xFmt |= X_ITA
- elif tFmt[i] == "i":
+ elif fFmt == self.FMT_I_E:
xFmt &= M_ITA
- elif tFmt[i] == "S":
+ elif fFmt == self.FMT_D_B:
xFmt |= X_DEL
- elif tFmt[i] == "s":
+ elif fFmt == self.FMT_D_E:
xFmt &= M_DEL
- elif tFmt[i] == "U":
+ elif fFmt == self.FMT_U_B:
xFmt |= X_UND
- elif tFmt[i] == "u":
+ elif fFmt == self.FMT_U_E:
xFmt &= M_UND
- elif tFmt[i] == "P":
+ elif fFmt == self.FMT_SUP_B:
xFmt |= X_SUP
- elif tFmt[i] == "p":
+ elif fFmt == self.FMT_SUP_E:
xFmt &= M_SUP
- elif tFmt[i] == "D":
+ elif fFmt == self.FMT_SUB_B:
xFmt |= X_SUB
- elif tFmt[i] == "d":
+ elif fFmt == self.FMT_SUB_E:
xFmt &= M_SUB
else:
pErr += 1
- if xFmt != pFmt:
- if pFmt == 0x00:
- parProc.appendText(tTemp)
- tTemp = ""
- else:
- parProc.appendSpan(tTemp, self._textStyle(pFmt))
- tTemp = ""
+ fLast = fPos
- pFmt = xFmt
-
- # Save what remains in the buffer
- if pFmt == 0x00:
- parProc.appendText(tTemp)
- else:
- parProc.appendSpan(tTemp, self._textStyle(pFmt))
+ if tFrag := tText[fLast:]:
+ if xFmt == 0x00:
+ parProc.appendText(tFrag)
+ else:
+ parProc.appendSpan(tFrag, self._textStyle(xFmt))
if pErr > 0:
self._errData.append("Unknown format tag encountered")