From b9df6259146dc98feadd4600bcbb713b5668b187 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 20 Oct 2023 21:42:00 +0200 Subject: [PATCH] Add ODT support for new formats --- novelwriter/core/tohtml.py | 21 ++++- novelwriter/core/toodt.py | 178 ++++++++++++++----------------------- 2 files changed, 86 insertions(+), 113 deletions(-) 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")