diff --git a/docs/source/int_customise.rst b/docs/source/int_customise.rst index d54afc2a..15a7a348 100644 --- a/docs/source/int_customise.rst +++ b/docs/source/int_customise.rst @@ -159,6 +159,7 @@ A syntax theme ``.conf`` file consists of the follwing settings: doublequotes = 0, 0, 0 singlequotes = 0, 0, 0 hidden = 0, 0, 0 + shortcode = 0, 0, 0 keyword = 0, 0, 0 value = 0, 0, 0 spellcheckline = 0, 0, 0 @@ -169,3 +170,7 @@ A syntax theme ``.conf`` file consists of the follwing settings: In the Main section, you must define at least the ``name`` setting. The Syntax colour values are RGB numbers of the format ``r, g, b`` where each is an integer from to 255. Omitted values default to black, except ``background`` which defaults to white, + +.. versionadded:: 2.2 + The `shortcode` syntax colour entry was added, so you need to update your custom themes if you + made any before version 2.2. diff --git a/novelwriter/assets/syntax/default_dark.conf b/novelwriter/assets/syntax/default_dark.conf index f015e5d6..77dc829e 100644 --- a/novelwriter/assets/syntax/default_dark.conf +++ b/novelwriter/assets/syntax/default_dark.conf @@ -17,6 +17,7 @@ straightquotes = 200, 46, 0 doublequotes = 184, 200, 0 singlequotes = 136, 200, 0 hidden = 150, 150, 150 +shortcode = 0, 155, 200 keyword = 200, 46, 0 value = 184, 200, 0 spellcheckline = 200, 46, 0 diff --git a/novelwriter/assets/syntax/default_light.conf b/novelwriter/assets/syntax/default_light.conf index 319fbda3..b4eab626 100644 --- a/novelwriter/assets/syntax/default_light.conf +++ b/novelwriter/assets/syntax/default_light.conf @@ -17,6 +17,7 @@ straightquotes = 200, 0, 0 doublequotes = 0, 0, 200 singlequotes = 0, 0, 200 hidden = 100, 100, 100 +shortcode = 0, 100, 0 keyword = 200, 50, 50 value = 50, 150, 50 spellcheckline = 200, 0, 0 diff --git a/novelwriter/assets/syntax/grey_dark.conf b/novelwriter/assets/syntax/grey_dark.conf index e579e8da..ed09963f 100644 --- a/novelwriter/assets/syntax/grey_dark.conf +++ b/novelwriter/assets/syntax/grey_dark.conf @@ -17,6 +17,7 @@ straightquotes = 200, 200, 200 doublequotes = 200, 200, 200 singlequotes = 200, 200, 200 hidden = 150, 150, 150 +shortcode = 225, 225, 225 keyword = 225, 225, 225 value = 200, 200, 200 spellcheckline = 200, 46, 0 diff --git a/novelwriter/assets/syntax/grey_light.conf b/novelwriter/assets/syntax/grey_light.conf index 32f9561c..e2ca0666 100644 --- a/novelwriter/assets/syntax/grey_light.conf +++ b/novelwriter/assets/syntax/grey_light.conf @@ -17,6 +17,7 @@ straightquotes = 20, 20, 20 doublequotes = 20, 20, 20 singlequotes = 20, 20, 20 hidden = 100, 100, 100 +shortcode = 0, 0, 0 keyword = 0, 0, 0 value = 20, 20, 20 spellcheckline = 200, 0, 0 diff --git a/novelwriter/assets/syntax/light_owl.conf b/novelwriter/assets/syntax/light_owl.conf index fdd0dc67..c42b3155 100644 --- a/novelwriter/assets/syntax/light_owl.conf +++ b/novelwriter/assets/syntax/light_owl.conf @@ -37,6 +37,7 @@ straightquotes = 222, 61, 58 doublequotes = 8, 145, 106 singlequotes = 255, 235, 149 hidden = 152, 159, 177 +shortcode = 40, 142, 215 keyword = 222, 61, 58 value = 150, 74, 193 spellcheckline = 222, 61, 58 diff --git a/novelwriter/assets/syntax/night_owl.conf b/novelwriter/assets/syntax/night_owl.conf index 59ad10da..2ee057e3 100644 --- a/novelwriter/assets/syntax/night_owl.conf +++ b/novelwriter/assets/syntax/night_owl.conf @@ -37,6 +37,7 @@ straightquotes = 247, 140, 108 doublequotes = 173, 219, 103 singlequotes = 255, 235, 149 hidden = 99, 119, 119 +shortcode = 130, 170, 255 keyword = 247, 140, 108 value = 199, 146, 234 spellcheckline = 247, 140, 108 diff --git a/novelwriter/assets/syntax/solarized_dark.conf b/novelwriter/assets/syntax/solarized_dark.conf index ec8a3d8d..ef34ddf4 100644 --- a/novelwriter/assets/syntax/solarized_dark.conf +++ b/novelwriter/assets/syntax/solarized_dark.conf @@ -17,6 +17,7 @@ straightquotes = 211, 54, 130 doublequotes = 42, 161, 152 singlequotes = 42, 161, 152 hidden = 147, 161, 161 +shortcode = 147, 161, 161 keyword = 133, 153, 0 value = 203, 75, 22 spellcheckline = 203, 75, 22 diff --git a/novelwriter/assets/syntax/solarized_light.conf b/novelwriter/assets/syntax/solarized_light.conf index 319a084c..21a31ade 100644 --- a/novelwriter/assets/syntax/solarized_light.conf +++ b/novelwriter/assets/syntax/solarized_light.conf @@ -17,6 +17,7 @@ straightquotes = 211, 54, 130 doublequotes = 42, 161, 152 singlequotes = 42, 161, 152 hidden = 88, 110, 117 +shortcode = 88, 110, 117 keyword = 133, 153, 0 value = 203, 75, 22 spellcheckline = 203, 75, 22 diff --git a/novelwriter/assets/syntax/tomorrow.conf b/novelwriter/assets/syntax/tomorrow.conf index 6333f06e..20430fcd 100644 --- a/novelwriter/assets/syntax/tomorrow.conf +++ b/novelwriter/assets/syntax/tomorrow.conf @@ -37,6 +37,7 @@ straightquotes = 240, 40, 41 doublequotes = 113, 140, 0 singlequotes = 234, 183, 0 hidden = 142, 144, 140 +shortcode = 66, 113, 174 keyword = 240, 40, 41 value = 137, 89, 168 spellcheckline = 240, 40, 41 diff --git a/novelwriter/assets/syntax/tomorrow_night.conf b/novelwriter/assets/syntax/tomorrow_night.conf index 2a4e8751..97c217df 100644 --- a/novelwriter/assets/syntax/tomorrow_night.conf +++ b/novelwriter/assets/syntax/tomorrow_night.conf @@ -37,6 +37,7 @@ straightquotes = 204, 102, 102 doublequotes = 181, 189, 104 singlequotes = 240, 198, 116 hidden = 150, 152, 150 +shortcode = 129, 162, 190 keyword = 204, 102, 102 value = 178, 148, 187 spellcheckline = 204, 102, 102 diff --git a/novelwriter/assets/syntax/tomorrow_night_blue.conf b/novelwriter/assets/syntax/tomorrow_night_blue.conf index 1a8459bf..18c930e7 100644 --- a/novelwriter/assets/syntax/tomorrow_night_blue.conf +++ b/novelwriter/assets/syntax/tomorrow_night_blue.conf @@ -37,6 +37,7 @@ straightquotes = 255, 157, 164 doublequotes = 209, 241, 169 singlequotes = 255, 238, 173 hidden = 114, 133, 183 +shortcode = 187, 218, 255 keyword = 255, 157, 164 value = 235, 187, 255 spellcheckline = 255, 157, 164 diff --git a/novelwriter/assets/syntax/tomorrow_night_bright.conf b/novelwriter/assets/syntax/tomorrow_night_bright.conf index c5bccc99..b7bf5658 100644 --- a/novelwriter/assets/syntax/tomorrow_night_bright.conf +++ b/novelwriter/assets/syntax/tomorrow_night_bright.conf @@ -37,6 +37,7 @@ straightquotes = 213, 78, 83 doublequotes = 185, 202, 74 singlequotes = 231, 197, 71 hidden = 150, 152, 150 +shortcode = 122, 166, 218 keyword = 213, 78, 83 value = 195, 151, 216 spellcheckline = 213, 78, 83 diff --git a/novelwriter/assets/syntax/tomorrow_night_eighties.conf b/novelwriter/assets/syntax/tomorrow_night_eighties.conf index c7314f17..29dbecc5 100644 --- a/novelwriter/assets/syntax/tomorrow_night_eighties.conf +++ b/novelwriter/assets/syntax/tomorrow_night_eighties.conf @@ -37,6 +37,7 @@ straightquotes = 242, 119, 122 doublequotes = 153, 204, 153 singlequotes = 255, 204, 102 hidden = 153, 153, 153 +shortcode = 102, 153, 204 keyword = 242, 119, 122 value = 204, 153, 204 spellcheckline = 242, 119, 122 diff --git a/novelwriter/constants.py b/novelwriter/constants.py index b1855f98..9567c3bb 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -23,6 +23,8 @@ along with this program. If not, see . """ from __future__ import annotations +import re + from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline @@ -59,6 +61,11 @@ class nwRegEx: FMT_EI = r"(? tuple[int, int, int]: if nwUnicode.U_EMDASH in text: text = text.replace(nwUnicode.U_EMDASH, " ") + # Strip shortcodes + if "[" in text: + text = nwRegEx.RX_SC.sub("", text) + for line in text.splitlines(): countPara = True @@ -1241,9 +1245,10 @@ def countWords(text: str) -> tuple[int, int, int]: continue if line[0] == "[": - if line.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")): + check = line.lower() + if check.startswith(("[newpage]", "[new page]", "[vspace]")): continue - elif line.startswith("[VSPACE:") and line.endswith("]"): + elif check.startswith("[vspace:") and line.endswith("]"): continue elif line[0] == "#": diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index d312767b..66515b26 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -31,7 +31,7 @@ from pathlib import Path from novelwriter import CONFIG from novelwriter.common import formatTimeStamp -from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode +from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode, trConst from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer, stripEscape @@ -131,6 +131,8 @@ class ToHtml(Tokenizer): self.FMT_I_E: "", self.FMT_D_B: "", self.FMT_D_E: "", + self.FMT_U_B: "", + self.FMT_U_E: "", } else: htmlTags = { # HTML5 (for export) @@ -140,8 +142,15 @@ class ToHtml(Tokenizer): self.FMT_I_E: "", self.FMT_D_B: "", self.FMT_D_E: "", + self.FMT_U_B: "", + self.FMT_U_E: "", } + htmlTags[self.FMT_SUP_B] = "" + htmlTags[self.FMT_SUP_E] = "" + htmlTags[self.FMT_SUB_B] = "" + htmlTags[self.FMT_SUB_E] = "" + if self._isNovel and self._genMode != self.M_PREVIEW: # For story files, we bump the titles one level up h1Cl = " class='title'" @@ -172,18 +181,16 @@ class ToHtml(Tokenizer): for c in tText: if c == "<": cText.append("<") - tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat] + tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat] i += 4 elif c == ">": cText.append(">") - tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat] + 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(">", ">") @@ -275,8 +282,8 @@ class ToHtml(Tokenizer): tTemp = tText if pStyle is None: pStyle = hStyle - for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:] + for pos, fmt in reversed(tFormat): + tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}" para.append(stripEscape(tTemp.rstrip())) elif tType == self.T_SYNOPSIS and self._doSynopsis: @@ -467,25 +474,18 @@ class ToHtml(Tokenizer): def _formatKeywords(self, text: str) -> str: """Apply HTML formatting to keywords.""" valid, bits, _ = self._project.index.scanThis("@"+text) - if not valid or not bits: + if not valid or not bits or bits[0] not in nwLabels.KEY_NAME: return "" - result = "" - tags = [] - if bits[0] in nwLabels.KEY_NAME: - result += f"{nwLabels.KEY_NAME[bits[0]]}: " - if len(bits) > 1: - if bits[0] == nwKeyWords.TAG_KEY: - result += f"{bits[1]}" + result = f"{trConst(nwLabels.KEY_NAME[bits[0]])}: " + if len(bits) > 1: + if bits[0] == nwKeyWords.TAG_KEY: + result += f"{bits[1]}" + else: + if self._genMode == self.M_PREVIEW: + result += ", ".join(f"{t}" for t in bits[1:]) else: - if self._genMode == self.M_PREVIEW: - for tTag in bits[1:]: - tags.append(f"{tTag}") - result += ", ".join(tags) - else: - for tTag in bits[1:]: - tags.append(f"{tTag}") - result += ", ".join(tags) + result += ", ".join(f"{t}" for t in bits[1:]) return result diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index cc0becf1..c1a51181 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -31,7 +31,6 @@ import logging from abc import ABC, abstractmethod from time import time from pathlib import Path -from operator import itemgetter from functools import partial from PyQt5.QtCore import QCoreApplication, QRegularExpression @@ -43,15 +42,14 @@ from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) +ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""} +RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL) + def stripEscape(text) -> str: - """Helper function to strip escaped Markdown characters from - paragraph text. - """ + """Strip escaped Markdown characters from paragraph text.""" if "\\" in text: - # Checking first is slightly slower when there are escaped - # characters in the text, but significantly faster when not - return text.replace(r"\*", "*").replace(r"\~", "~").replace(r"\_", "_") + return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text) return text @@ -65,12 +63,18 @@ class Tokenizer(ABC): """ # In-Text Format - FMT_B_B = 1 # Begin bold - FMT_B_E = 2 # End bold - FMT_I_B = 3 # Begin italics - FMT_I_E = 4 # End italics - FMT_D_B = 5 # Begin strikeout - FMT_D_E = 6 # End strikeout + FMT_B_B = 1 # Begin bold + FMT_B_E = 2 # End bold + FMT_I_B = 3 # Begin italics + FMT_I_E = 4 # End italics + FMT_D_B = 5 # Begin strikeout + FMT_D_E = 6 # End strikeout + FMT_U_B = 7 # Begin underline + FMT_U_E = 8 # End underline + FMT_SUP_B = 9 # Begin superscript + FMT_SUP_E = 10 # End superscript + FMT_SUB_B = 11 # Begin subscript + FMT_SUB_E = 12 # End subscript # Block Type T_EMPTY = 1 # Empty line (new paragraph) @@ -166,6 +170,24 @@ class Tokenizer(ABC): # Cached Translations self._trSynopsis = self.tr("Synopsis") + # Format RegEx + self._rxMarkdown = [ + (QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]), + (QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]), + (QRegularExpression(nwRegEx.FMT_ST), [0, self.FMT_D_B, 0, self.FMT_D_E]), + ] + self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC) + self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV) + + self._shortCodeFmt = { + "[i]": self.FMT_I_B, "[/i]": self.FMT_I_E, + "[b]": self.FMT_B_B, "[/b]": self.FMT_B_E, + "[s]": self.FMT_D_B, "[/s]": self.FMT_D_E, + "[u]": self.FMT_U_B, "[/u]": self.FMT_U_E, + "[sup]": self.FMT_SUP_B, "[/sup]": self.FMT_SUP_E, + "[sub]": self.FMT_SUB_B, "[/sub]": self.FMT_SUB_E, + } + return ## @@ -387,19 +409,12 @@ class Tokenizer(ABC): 4: The internal formatting map of the text, self.FMT_* 5: The style of the block, self.A_* """ - # RegExes for adding formatting tags within text lines - rxFormats = [ - (QRegularExpression(nwRegEx.FMT_EI), [None, self.FMT_I_B, None, self.FMT_I_E]), - (QRegularExpression(nwRegEx.FMT_EB), [None, self.FMT_B_B, None, self.FMT_B_E]), - (QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]), - ] - self._tokens = [] tmpMarkdown = [] nHead = 0 breakNext = False for aLine in self._text.splitlines(): - sLine = aLine.strip() + sLine = aLine.strip().lower() # Check for blank lines if len(sLine) == 0: @@ -422,18 +437,21 @@ class Tokenizer(ABC): if aLine[0] == "[": # Parse special formatting line + # This must be a separate if statement, as it may not + # reach a continue statement and must thefore proceed to + # check other formats. - if sLine in ("[NEWPAGE]", "[NEW PAGE]"): + if sLine in ("[newpage]", "[new page]"): breakNext = True continue - elif sLine == "[VSPACE]": + elif sLine == "[vspace]": self._tokens.append( (self.T_SKIP, nHead, "", None, sAlign) ) continue - elif sLine.startswith("[VSPACE:") and sLine.endswith("]"): + elif sLine.startswith("[vspace:") and sLine.endswith("]"): nSkip = checkInt(sLine[8:-1], 0) if nSkip >= 1: self._tokens.append( @@ -445,7 +463,7 @@ class Tokenizer(ABC): ] continue - elif aLine[0] == "%": + if aLine[0] == "%": cLine = aLine[1:].lstrip() synTag = cLine[:9].lower() if synTag == "synopsis:": @@ -570,23 +588,10 @@ class Tokenizer(ABC): if indRight: sAlign |= self.A_IND_R - # Otherwise we use RegEx to find formatting tags within a line of text - fmtPos = [] - for theRX, theKeys in rxFormats: - rxThis = theRX.globalMatch(aLine, 0) - while rxThis.hasNext(): - rxMatch = rxThis.next() - for n in range(1, len(theKeys)): - if theKeys[n] is not None: - xPos = rxMatch.capturedStart(n) - xLen = rxMatch.capturedLength(n) - fmtPos.append([xPos, xLen, theKeys[n]]) - - # Save the line as is, but append the array of formatting locations - # sorted by position - fmtPos = sorted(fmtPos, key=itemgetter(0)) + # Process formats + tLine, fmtPos = self._extractFormats(aLine) self._tokens.append(( - self.T_TEXT, nHead, aLine, fmtPos, sAlign + self.T_TEXT, nHead, tLine, fmtPos, sAlign )) if self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) @@ -763,6 +768,45 @@ class Tokenizer(ABC): json.dump(data, fObj, indent=2) return + ## + # Internal Functions + ## + + def _extractFormats(self, text: str) -> tuple[str, list[tuple[int, int]]]: + """Extract format markers from a text paragraph.""" + temp = [] + + # Match Markdown + for regEx, fmts in self._rxMarkdown: + rxItt = regEx.globalMatch(text, 0) + while rxItt.hasNext(): + rxMatch = rxItt.next() + temp.extend( + [rxMatch.capturedStart(n), rxMatch.capturedLength(n), fmt] + for n, fmt in enumerate(fmts) if fmt > 0 + ) + + # Match Shortcodes + rxItt = self._rxShortCodes.globalMatch(text, 0) + while rxItt.hasNext(): + rxMatch = rxItt.next() + temp.append([ + rxMatch.capturedStart(1), + rxMatch.capturedLength(1), + self._shortCodeFmt.get(rxMatch.captured(1).lower(), 0) + ]) + + # Post-process text and format markers + result = text + formats = [] + for pos, n, fmt in reversed(sorted(temp, key=lambda x: x[0])): + if fmt > 0: + result = result[:pos] + result[pos+n:] + formats = [(p-n, f) for p, f in formats] + formats.insert(0, (pos, fmt)) + + return result, formats + # END Class Tokenizer diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py index ec1646ad..cc2e8696 100644 --- a/novelwriter/core/tomd.py +++ b/novelwriter/core/tomd.py @@ -27,7 +27,7 @@ import logging from pathlib import Path -from novelwriter.constants import nwHeadFmt, nwLabels +from novelwriter.constants import nwHeadFmt, nwLabels, trConst from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer @@ -38,12 +38,12 @@ class ToMarkdown(Tokenizer): """Core: Markdown Document Writer Extend the Tokenizer class to writer Markdown output. It supports - both Standard Markdown and GitHub Flavour Markdown (Extended). The - class also supports concatenating novelWriter markup files. + both Standard Markdown and Extended Markdown. The class also + supports concatenating novelWriter markup files. """ M_STD = 0 # Standard Markdown - M_GH = 1 # GitHub Markdown + M_EXT = 1 # Extended Markdown def __init__(self, project: NWProject) -> None: super().__init__(project) @@ -69,9 +69,9 @@ class ToMarkdown(Tokenizer): self._genMode = self.M_STD return - def setGitHubMarkdown(self) -> None: - """Set the converter to use GitHub Markdown formatting.""" - self._genMode = self.M_GH + def setExtendedMarkdown(self) -> None: + """Set the converter to use Extended Markdown formatting.""" + self._genMode = self.M_EXT return ## @@ -85,7 +85,7 @@ class ToMarkdown(Tokenizer): def doConvert(self) -> None: """Convert the list of text tokens into a Markdown document.""" if self._genMode == self.M_STD: - # Standard + # Standard Markdown mdTags = { self.FMT_B_B: "**", self.FMT_B_E: "**", @@ -93,9 +93,15 @@ class ToMarkdown(Tokenizer): self.FMT_I_E: "_", self.FMT_D_B: "", self.FMT_D_E: "", + self.FMT_U_B: "", + self.FMT_U_E: "", + self.FMT_SUP_B: "", + self.FMT_SUP_E: "", + self.FMT_SUB_B: "", + self.FMT_SUB_E: "", } else: - # GitHub + # Extended Markdown mdTags = { self.FMT_B_B: "**", self.FMT_B_E: "**", @@ -103,6 +109,12 @@ class ToMarkdown(Tokenizer): self.FMT_I_E: "_", self.FMT_D_B: "~~", self.FMT_D_E: "~~", + self.FMT_U_B: "", + self.FMT_U_E: "", + self.FMT_SUP_B: "^", + self.FMT_SUP_E: "^", + self.FMT_SUB_B: "~", + self.FMT_SUB_E: "~", } self._result = "" @@ -150,8 +162,8 @@ class ToMarkdown(Tokenizer): elif tType == self.T_TEXT: tTemp = tText - for xPos, xLen, xFmt in reversed(tFormat): - tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:] + for pos, fmt in reversed(tFormat): + tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}" para.append(tTemp.rstrip()) elif tType == self.T_SYNOPSIS and self._doSynopsis: @@ -197,7 +209,7 @@ class ToMarkdown(Tokenizer): result = "" if bits[0] in nwLabels.KEY_NAME: - result += f"**{nwLabels.KEY_NAME[bits[0]]}:** " + result += f"**{trConst(nwLabels.KEY_NAME[bits[0]])}:** " if len(bits) > 1: result += ", ".join(bits[1:]) diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index c4f6b19d..c1165e30 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -29,6 +29,7 @@ from __future__ import annotations import logging import xml.etree.ElementTree as ET +from typing import Sequence from hashlib import sha256 from pathlib import Path from zipfile import ZipFile @@ -36,7 +37,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, nwKeyWords, nwLabels, trConst from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer, stripEscape @@ -82,11 +83,17 @@ TAG_STNM = _mkTag("text", "style-name") X_BLD = 0x01 # Bold format X_ITA = 0x02 # Italic format X_DEL = 0x04 # Strikethrough format +X_UND = 0x08 # Underline format +X_SUP = 0x10 # Superscript +X_SUB = 0x20 # Subscript # Formatting Masks M_BLD = ~X_BLD M_ITA = ~X_ITA M_DEL = ~X_DEL +M_UND = ~X_UND +M_SUP = ~X_SUP +M_SUB = ~X_SUB class ToOdt(Tokenizer): @@ -385,17 +392,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 - } - - fmt = [] - para = [] + pFmt = [] + pText = [] pStyle = None for tType, _, tText, tFormat, tStyle in self._tokens: @@ -429,19 +427,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: @@ -477,15 +477,8 @@ class ToOdt(Tokenizer): elif tType == self.T_TEXT: if pStyle is None: pStyle = oStyle - - tFmt = " "*len(tText) - for xPos, xLen, xFmt in tFormat: - tFmt = tFmt[:xPos] + odtTags[xFmt] + 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) @@ -559,46 +552,39 @@ 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 "", "" + if not valid or not bits or bits[0] not in nwLabels.KEY_NAME: + 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)) + rTxt = f"{trConst(nwLabels.KEY_NAME[bits[0]])}: " + rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)] + if len(bits) > 1: + if bits[0] == nwKeyWords.TAG_KEY: + rTxt += bits[1] + else: + rTxt += ", ".join(bits[1:]) 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: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None ) -> None: """Add a text paragraph to the text XML element.""" tAttr = {} @@ -616,58 +602,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 fFmt == self.FMT_U_B: + xFmt |= X_UND + elif fFmt == self.FMT_U_E: + xFmt &= M_UND + elif fFmt == self.FMT_SUP_B: + xFmt |= X_SUP + elif fFmt == self.FMT_SUP_E: + xFmt &= M_SUP + elif fFmt == self.FMT_SUB_B: + xFmt |= X_SUB + 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") @@ -713,6 +700,14 @@ class ToOdt(Tokenizer): if hFmt & X_DEL: newStyle.setStrikeStyle("solid") newStyle.setStrikeType("single") + if hFmt & X_UND: + newStyle.setUnderlineStyle("solid") + newStyle.setUnderlineWidth("auto") + newStyle.setUnderlineColour("font-color") + if hFmt & X_SUP: + newStyle.setTextPosition("super") + if hFmt & X_SUB: + newStyle.setTextPosition("sub") self._autoText[hFmt] = (newName, newStyle) @@ -1256,16 +1251,23 @@ class ODTTextStyle: """ VALID_WEIGHT = ["normal", "inherit", "bold"] VALID_STYLE = ["normal", "inherit", "italic"] + VALID_POS = ["super", "sub"] VALID_LSTYLE = ["none", "solid"] - VALID_LTYPE = ["none", "single", "double"] + VALID_LTYPE = ["single", "double"] + VALID_LWIDTH = ["auto"] + VALID_LCOL = ["font-color"] def __init__(self) -> None: # Text Attributes self._tAttr = { "font-weight": ["fo", None], "font-style": ["fo", None], + "text-position": ["style", None], "text-line-through-style": ["style", None], "text-line-through-type": ["style", None], + "text-underline-style": ["style", None], + "text-underline-width": ["style", None], + "text-underline-color": ["style", None], } return @@ -1287,6 +1289,13 @@ class ODTTextStyle: self._tAttr["font-style"][1] = None return + def setTextPosition(self, value: str | None) -> None: + if value in self.VALID_POS: + self._tAttr["text-position"][1] = f"{value} 58%" + else: + self._tAttr["text-position"][1] = None + return + def setStrikeStyle(self, value: str | None) -> None: if value in self.VALID_LSTYLE: self._tAttr["text-line-through-style"][1] = value @@ -1301,6 +1310,27 @@ class ODTTextStyle: self._tAttr["text-line-through-type"][1] = None return + def setUnderlineStyle(self, value: str | None) -> None: + if value in self.VALID_LSTYLE: + self._tAttr["text-underline-style"][1] = value + else: + self._tAttr["text-underline-style"][1] = None + return + + def setUnderlineWidth(self, value: str | None) -> None: + if value in self.VALID_LWIDTH: + self._tAttr["text-underline-width"][1] = value + else: + self._tAttr["text-underline-width"][1] = None + return + + def setUnderlineColour(self, value: str | None) -> None: + if value in self.VALID_LCOL: + self._tAttr["text-underline-color"][1] = value + else: + self._tAttr["text-underline-color"][1] = None + return + ## # Methods ## diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 1555e61e..93053f20 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -55,6 +55,7 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.common import minmax, transferCase from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.core.index import countWords +from novelwriter.core.document import NWDocument from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.editordocument import GuiTextDocument from novelwriter.extensions.wheeleventfilter import WheelEventFilter @@ -408,12 +409,6 @@ class GuiDocEditor(QPlainTextEdit): self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META) return - def redrawText(self) -> None: - """Redraw the text by marking the document content as dirty.""" - self._qDocument.markContentsDirty(0, self._qDocument.characterCount()) - self.updateDocMargins() - return - def replaceText(self, text: str) -> None: """Replace the text of the current document with the provided text. This also clears undo history. @@ -735,19 +730,17 @@ class GuiDocEditor(QPlainTextEdit): """Tell the user where on the file system the file in the editor is saved. """ - if self._nwDocument is None: - logger.error("No document open") - return - SHARED.info( - "
".join([ - self.tr("Document Details"), - "–"*40, - self.tr("Created: {0}").format(self._nwDocument.createdDate), - self.tr("Updated: {0}").format(self._nwDocument.updatedDate), - ]), - details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation), - log=False - ) + if isinstance(self._nwDocument, NWDocument): + SHARED.info( + "
".join([ + self.tr("Document Details"), + "–"*40, + self.tr("Created: {0}").format(self._nwDocument.createdDate), + self.tr("Updated: {0}").format(self._nwDocument.updatedDate), + ]), + details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation), + log=False + ) return def insertText(self, insert: str | nwDocInsert) -> bool: @@ -775,15 +768,15 @@ class GuiDocEditor(QPlainTextEdit): newBlock = True goAfter = True elif insert == nwDocInsert.NEW_PAGE: - text = "[NEW PAGE]" + text = "[newpage]" newBlock = True goAfter = False elif insert == nwDocInsert.VSPACE_S: - text = "[VSPACE]" + text = "[vspace]" newBlock = True goAfter = False elif insert == nwDocInsert.VSPACE_M: - text = "[VSPACE:2]" + text = "[vspace:2]" newBlock = True goAfter = False else: diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 5acd7ef7..6a77f267 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -39,7 +39,7 @@ from novelwriter.constants import nwRegEx, nwUnicode logger = logging.getLogger(__name__) -SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b") +SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b") SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) @@ -69,6 +69,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._colDialD = QColor(0, 0, 0) self._colDialS = QColor(0, 0, 0) self._colHidden = QColor(0, 0, 0) + self._colCode = QColor(0, 0, 0) self._colKey = QColor(0, 0, 0) self._colVal = QColor(0, 0, 0) self._colSpell = QColor(0, 0, 0) @@ -95,6 +96,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._colDialD = QColor(*SHARED.theme.colDialD) self._colDialS = QColor(*SHARED.theme.colDialS) self._colHidden = QColor(*SHARED.theme.colHidden) + self._colCode = QColor(*SHARED.theme.colCode) self._colKey = QColor(*SHARED.theme.colKey) self._colVal = QColor(*SHARED.theme.colVal) self._colSpell = QColor(*SHARED.theme.colSpell) @@ -127,6 +129,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): "dialogue3": self._makeFormat(self._colDialS), "replace": self._makeFormat(self._colRepTag), "hidden": self._makeFormat(self._colHidden), + "code": self._makeFormat(self._colCode), "keyword": self._makeFormat(self._colKey), "modifier": self._makeFormat(self._colMod), "value": self._makeFormat(self._colVal, "underline"), @@ -205,6 +208,22 @@ class GuiDocHighlighter(QSyntaxHighlighter): } )) + # Shortcodes + self._hRules.append(( + nwRegEx.FMT_SC, { + 1: self._hStyles["code"], + } + )) + + # Shortcodes w/Value + self._hRules.append(( + nwRegEx.FMT_SV, { + 1: self._hStyles["code"], + 2: self._hStyles["codevalue"], + 3: self._hStyles["code"], + } + )) + # Alignment Tags self._hRules.append(( r"(^>{1,2}|<{1,2}$)", { @@ -347,18 +366,17 @@ class GuiDocHighlighter(QSyntaxHighlighter): else: # Text Paragraph if text.startswith("["): # Special Command - sText = text.rstrip() - if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"): - self.setFormat(0, len(text), self._hStyles["keyword"]) + sText = text.rstrip().lower() + if sText in ("[newpage]", "[new page]", "[vspace]"): + self.setFormat(0, len(text), self._hStyles["code"]) return - - elif sText.startswith("[VSPACE:") and sText.endswith("]"): + elif sText.startswith("[vspace:") and sText.endswith("]"): tLen = len(sText) tVal = checkInt(sText[8:-1], 0) cVal = "codevalue" if tVal > 0 else "codeinval" - self.setFormat(0, 8, self._hStyles["keyword"]) + self.setFormat(0, 8, self._hStyles["code"]) self.setFormat(8, tLen-9, self._hStyles[cVal]) - self.setFormat(tLen-1, tLen, self._hStyles["keyword"]) + self.setFormat(tLen-1, tLen, self._hStyles["code"]) return # Regular Text diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index a91d9ae1..da8f1be4 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -97,6 +97,7 @@ class GuiTheme: self.colDialD = [0, 0, 0] self.colDialS = [0, 0, 0] self.colHidden = [0, 0, 0] + self.colCode = [0, 0, 0] self.colKey = [0, 0, 0] self.colVal = [0, 0, 0] self.colSpell = [0, 0, 0] @@ -316,6 +317,7 @@ class GuiTheme: self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes") self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes") self.colHidden = self._parseColour(confParser, cnfSec, "hidden") + self.colCode = self._parseColour(confParser, cnfSec, "shortcode") self.colKey = self._parseColour(confParser, cnfSec, "keyword") self.colVal = self._parseColour(confParser, cnfSec, "value") self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline") diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index de7f32d7..78c6d8b9 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,8 +1,8 @@ %%~name: Making a Scene %%~path: 6a2d6d5f4f401/636b6aa9b697b %%~kind: NOVEL/DOCUMENT -%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5 -%%~date: Unknown/2023-09-07 19:00:10 +%%~hash: d7920fe95fb48140278ae160fe7c3c1ce530072c +%%~date: Unknown/2023-10-20 22:57:56 ### Making a Scene @pov: Jane @@ -13,6 +13,8 @@ A scene is defined by a level three heading, like the one at the top of this pag Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesn’t show it correctly, the export tool will not either. +For special formatting aside from standard emphasis, you can use “shortcodes”. Here we have super[sup]script[/sup] and sub[sub]script[/sub], and [b]part[/b]ial bold too, which ends in the middle of a word. The shortcodes also support [u]underline[/u]. + In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings. diff --git a/sample/content/974e400180a99.nwd b/sample/content/974e400180a99.nwd index 0175798b..5058f0ca 100644 --- a/sample/content/974e400180a99.nwd +++ b/sample/content/974e400180a99.nwd @@ -1,11 +1,11 @@ %%~name: Page %%~path: 7031beac91f75/974e400180a99 %%~kind: NOVEL/DOCUMENT -%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139 -%%~date: Unknown/2023-08-25 16:51:54 -[NEW PAGE] -[VSPACE:2] +%%~hash: a0b0719e19763bf505bcf797bea0968c505c9b4a +%%~date: Unknown/2023-11-06 11:19:33 +[newpage] +[vspace:2] This is a plain page with some text on it. -If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. The above code adds two empty paragraphs before the text starts. +If you want the text to start on a fresh page, add the [newpage] code above the text. You can also add empty paragraphs with the [vspace] code. The above code adds two empty paragraphs before the text starts. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 00ea7567..67b86c1f 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Sample Project Jane Smith @@ -36,7 +36,7 @@ Main - + Novel @@ -46,7 +46,7 @@ Title Page - + Page @@ -58,7 +58,7 @@ Chapter One - + Making a Scene diff --git a/tests/reference/coreToOdt_SaveFlat_document.fodt b/tests/reference/coreToOdt_SaveFlat_document.fodt index 70198aaa..e684d070 100644 --- a/tests/reference/coreToOdt_SaveFlat_document.fodt +++ b/tests/reference/coreToOdt_SaveFlat_document.fodt @@ -1,13 +1,13 @@ - 2023-07-19T23:55:29 - novelWriter/2.1-beta1 + 2023-10-20T22:51:30 + novelWriter/2.2-alpha1 Jane Smith 1234 P42DT12H34M56S Test Project - 2023-07-19T23:55:29 + 2023-10-20T22:51:30 Jane Smith @@ -64,7 +64,7 @@ - + diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index d8b7917e..03b497da 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -1281,7 +1281,7 @@ def testCoreIndex_CountWords(): assert wC == 9 assert pC == 4 - # Formatting Codes + # Formatting Codes, Upper Case (Old Implementation) cC, wC, pC = countWords(( "Some text\n\n" "[NEWPAGE]\n\n" @@ -1297,4 +1297,29 @@ def testCoreIndex_CountWords(): assert wC == 13 assert pC == 5 + # Formatting Codes, Lower Case (Current Implementation) + cC, wC, pC = countWords(( + "Some text\n\n" + "[newpage]\n\n" + "more text\n\n" + "[new page]]\n\n" + "even more text\n\n" + "[vspace]\n\n" + "and some final text\n\n" + "[vspace:4]\n\n" + "THE END\n\n" + )) + assert cC == 58 + assert wC == 13 + assert pC == 5 + + # Check ShortCodes + cC, wC, pC = countWords(( + "Text with [b]bold[/b] text and padded [b] bold [/b] text.\n\n" + "Text with [b][i] nested [/i] emphasis [/b] in it.\n\n" + )) + assert cC == 78 + assert wC == 14 + assert pC == 2 + # END Test testCoreIndex_CountWords diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index ed13e05c..d5cbee87 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import json import pytest from tools import C, buildTestProject, readFile @@ -36,96 +37,96 @@ class BareTokenizer(Tokenizer): @pytest.mark.core def testCoreToken_Setters(mockGUI): """Test all the setters for the Tokenizer class.""" - theProject = NWProject() - theToken = BareTokenizer(theProject) + project = NWProject() + tokens = BareTokenizer(project) # Verify defaults - assert theToken._fmtTitle == nwHeadFmt.TITLE - assert theToken._fmtChapter == nwHeadFmt.TITLE - assert theToken._fmtUnNum == nwHeadFmt.TITLE - assert theToken._fmtScene == nwHeadFmt.TITLE - assert theToken._fmtSection == nwHeadFmt.TITLE - assert theToken._textFont == "Serif" - assert theToken._textSize == 11 - assert theToken._textFixed is False - assert theToken._lineHeight == 1.15 - assert theToken._blockIndent == 4.0 - assert theToken._doJustify is False - assert theToken._marginTitle == (1.000, 0.500) - assert theToken._marginHead1 == (1.000, 0.500) - assert theToken._marginHead2 == (0.834, 0.500) - assert theToken._marginHead3 == (0.584, 0.500) - assert theToken._marginHead4 == (0.584, 0.500) - assert theToken._marginText == (0.000, 0.584) - assert theToken._marginMeta == (0.000, 0.584) - assert theToken._hideScene is False - assert theToken._hideSection is False - assert theToken._linkHeaders is False - assert theToken._doBodyText is True - assert theToken._doSynopsis is False - assert theToken._doComments is False - assert theToken._doKeywords is False + assert tokens._fmtTitle == nwHeadFmt.TITLE + assert tokens._fmtChapter == nwHeadFmt.TITLE + assert tokens._fmtUnNum == nwHeadFmt.TITLE + assert tokens._fmtScene == nwHeadFmt.TITLE + assert tokens._fmtSection == nwHeadFmt.TITLE + assert tokens._textFont == "Serif" + assert tokens._textSize == 11 + assert tokens._textFixed is False + assert tokens._lineHeight == 1.15 + assert tokens._blockIndent == 4.0 + assert tokens._doJustify is False + assert tokens._marginTitle == (1.000, 0.500) + assert tokens._marginHead1 == (1.000, 0.500) + assert tokens._marginHead2 == (0.834, 0.500) + assert tokens._marginHead3 == (0.584, 0.500) + assert tokens._marginHead4 == (0.584, 0.500) + assert tokens._marginText == (0.000, 0.584) + assert tokens._marginMeta == (0.000, 0.584) + assert tokens._hideScene is False + assert tokens._hideSection is False + assert tokens._linkHeaders is False + assert tokens._doBodyText is True + assert tokens._doSynopsis is False + assert tokens._doComments is False + assert tokens._doKeywords is False # Set new values - theToken.setTitleFormat(f"T: {nwHeadFmt.TITLE}") - theToken.setChapterFormat(f"C: {nwHeadFmt.TITLE}") - theToken.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}") - theToken.setSceneFormat(f"S: {nwHeadFmt.TITLE}", True) - theToken.setSectionFormat(f"X: {nwHeadFmt.TITLE}", True) - theToken.setFont("Monospace", 10, True) - theToken.setLineHeight(2.0) - theToken.setBlockIndent(6.0) - theToken.setJustify(True) - theToken.setTitleMargins(2.0, 2.0) - theToken.setHead1Margins(2.0, 2.0) - theToken.setHead2Margins(2.0, 2.0) - theToken.setHead3Margins(2.0, 2.0) - theToken.setHead4Margins(2.0, 2.0) - theToken.setTextMargins(2.0, 2.0) - theToken.setMetaMargins(2.0, 2.0) - theToken.setLinkHeaders(True) - theToken.setBodyText(False) - theToken.setSynopsis(True) - theToken.setComments(True) - theToken.setKeywords(True) + tokens.setTitleFormat(f"T: {nwHeadFmt.TITLE}") + tokens.setChapterFormat(f"C: {nwHeadFmt.TITLE}") + tokens.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}") + tokens.setSceneFormat(f"S: {nwHeadFmt.TITLE}", True) + tokens.setSectionFormat(f"X: {nwHeadFmt.TITLE}", True) + tokens.setFont("Monospace", 10, True) + tokens.setLineHeight(2.0) + tokens.setBlockIndent(6.0) + tokens.setJustify(True) + tokens.setTitleMargins(2.0, 2.0) + tokens.setHead1Margins(2.0, 2.0) + tokens.setHead2Margins(2.0, 2.0) + tokens.setHead3Margins(2.0, 2.0) + tokens.setHead4Margins(2.0, 2.0) + tokens.setTextMargins(2.0, 2.0) + tokens.setMetaMargins(2.0, 2.0) + tokens.setLinkHeaders(True) + tokens.setBodyText(False) + tokens.setSynopsis(True) + tokens.setComments(True) + tokens.setKeywords(True) # Check new values - assert theToken._fmtTitle == f"T: {nwHeadFmt.TITLE}" - assert theToken._fmtChapter == f"C: {nwHeadFmt.TITLE}" - assert theToken._fmtUnNum == f"U: {nwHeadFmt.TITLE}" - assert theToken._fmtScene == f"S: {nwHeadFmt.TITLE}" - assert theToken._fmtSection == f"X: {nwHeadFmt.TITLE}" - assert theToken._textFont == "Monospace" - assert theToken._textSize == 10 - assert theToken._textFixed is True - assert theToken._lineHeight == 2.0 - assert theToken._blockIndent == 6.0 - assert theToken._doJustify is True - assert theToken._marginTitle == (2.0, 2.0) - assert theToken._marginHead1 == (2.0, 2.0) - assert theToken._marginHead2 == (2.0, 2.0) - assert theToken._marginHead3 == (2.0, 2.0) - assert theToken._marginHead4 == (2.0, 2.0) - assert theToken._marginText == (2.0, 2.0) - assert theToken._marginMeta == (2.0, 2.0) - assert theToken._hideScene is True - assert theToken._hideSection is True - assert theToken._linkHeaders is True - assert theToken._doBodyText is False - assert theToken._doSynopsis is True - assert theToken._doComments is True - assert theToken._doKeywords is True + assert tokens._fmtTitle == f"T: {nwHeadFmt.TITLE}" + assert tokens._fmtChapter == f"C: {nwHeadFmt.TITLE}" + assert tokens._fmtUnNum == f"U: {nwHeadFmt.TITLE}" + assert tokens._fmtScene == f"S: {nwHeadFmt.TITLE}" + assert tokens._fmtSection == f"X: {nwHeadFmt.TITLE}" + assert tokens._textFont == "Monospace" + assert tokens._textSize == 10 + assert tokens._textFixed is True + assert tokens._lineHeight == 2.0 + assert tokens._blockIndent == 6.0 + assert tokens._doJustify is True + assert tokens._marginTitle == (2.0, 2.0) + assert tokens._marginHead1 == (2.0, 2.0) + assert tokens._marginHead2 == (2.0, 2.0) + assert tokens._marginHead3 == (2.0, 2.0) + assert tokens._marginHead4 == (2.0, 2.0) + assert tokens._marginText == (2.0, 2.0) + assert tokens._marginMeta == (2.0, 2.0) + assert tokens._hideScene is True + assert tokens._hideSection is True + assert tokens._linkHeaders is True + assert tokens._doBodyText is False + assert tokens._doSynopsis is True + assert tokens._doComments is True + assert tokens._doKeywords is True # Check Limits - theToken.setLineHeight(0.0) - assert theToken._lineHeight == 0.5 - theToken.setLineHeight(10.0) - assert theToken._lineHeight == 5.0 + tokens.setLineHeight(0.0) + assert tokens._lineHeight == 0.5 + tokens.setLineHeight(10.0) + assert tokens._lineHeight == 5.0 - theToken.setBlockIndent(-6.0) - assert theToken._blockIndent == 0.0 - theToken.setBlockIndent(60.0) - assert theToken._blockIndent == 10.0 + tokens.setBlockIndent(-6.0) + assert tokens._blockIndent == 0.0 + tokens.setBlockIndent(60.0) + assert tokens._blockIndent == 10.0 # END Test testCoreToken_Setters @@ -133,15 +134,15 @@ def testCoreToken_Setters(mockGUI): @pytest.mark.core def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath): """Test handling files and text in the Tokenizer class.""" - theProject = NWProject() + project = NWProject() mockRnd.reset() - buildTestProject(theProject, fncPath) + buildTestProject(project, fncPath) - theProject.data.setLanguage("en") - theProject._loadProjectLocalisation() + project.data.setLanguage("en") + project._loadProjectLocalisation() - theToken = BareTokenizer(theProject) - theToken.setKeepMarkdown(True) + tokens = BareTokenizer(project) + tokens.setKeepMarkdown(True) # Set some content to work with docText = ( @@ -153,58 +154,65 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath): ) docTextR = docText.replace("", "this").replace("", "that") - nDoc = theProject.storage.getDocument(C.hSceneDoc) + nDoc = project.storage.getDocument(C.hSceneDoc) assert nDoc.writeDocument(docText) - theProject.data.setAutoReplace({"A": "this", "B": "that"}) + project.data.setAutoReplace({"A": "this", "B": "that"}) - assert theProject.saveProject() + assert project.saveProject() # Root Heading - assert theToken.addRootHeading("stuff") is False - assert theToken.addRootHeading(C.hSceneDoc) is False + assert tokens.addRootHeading("stuff") is False + assert tokens.addRootHeading(C.hSceneDoc) is False # First Page - assert theToken.addRootHeading(C.hPlotRoot) is True - assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n" - assert theToken._tokens[-1] == ( + assert tokens.addRootHeading(C.hPlotRoot) is True + assert tokens.theMarkdown[-1] == "# Notes: Plot\n\n" + assert tokens._tokens[-1] == ( Tokenizer.T_TITLE, 0, "Notes: Plot", None, Tokenizer.A_CENTRE ) # Not First Page - assert theToken.addRootHeading(C.hPlotRoot) is True - assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n" - assert theToken._tokens[-1] == ( + assert tokens.addRootHeading(C.hPlotRoot) is True + assert tokens.theMarkdown[-1] == "# Notes: Plot\n\n" + assert tokens._tokens[-1] == ( Tokenizer.T_TITLE, 0, "Notes: Plot", None, Tokenizer.A_CENTRE | Tokenizer.A_PBB ) # Set Text - assert theToken.setText("stuff") is False - assert theToken.setText(C.hSceneDoc) is True - assert theToken._text == docText + assert tokens.setText("stuff") is False + assert tokens.setText(C.hSceneDoc) is True + assert tokens._text == docText - assert theToken.setText(C.hSceneDoc, docText) is True - assert theToken._text == docText + assert tokens.setText(C.hSceneDoc, docText) is True + assert tokens._text == docText - assert theToken._isNone is False - assert theToken._isNovel is True - assert theToken._isNote is False + assert tokens._isNone is False + assert tokens._isNovel is True + assert tokens._isNote is False # Pre Processing - theToken.doPreProcessing() - assert theToken._text == docTextR + tokens.doPreProcessing() + assert tokens._text == docTextR # Save File savePath = fncPath / "dump.nwd" - theToken.saveRawMarkdown(savePath) + tokens.saveRawMarkdown(savePath) assert readFile(savePath) == ( "# Notes: Plot\n\n" "# Notes: Plot\n\n" ) + tokens.saveRawMarkdownJSON(savePath) + assert json.loads(readFile(savePath))["text"] == { + "nwd": [ + ["# Notes: Plot"], + ["# Notes: Plot"] + ] + } # Check abstract method with pytest.raises(NotImplementedError): - theToken.doConvert() + tokens.doConvert() # END Test testCoreToken_TextOps @@ -223,202 +231,202 @@ def testCoreToken_StripEscape(): @pytest.mark.core def testCoreToken_HeaderFormat(mockGUI): """Test the tokenization of header formats in the Tokenizer class.""" - theProject = NWProject() - theToken = BareTokenizer(theProject) - theToken.setKeepMarkdown(True) + project = NWProject() + tokens = BareTokenizer(project) + tokens.setKeepMarkdown(True) # Title # ===== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._isFirst = True - theToken._text = "#! Novel Title\n" + tokens._isNovel = True + tokens._isNote = False + tokens._isFirst = True + tokens._text = "#! Novel Title\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "#! Novel Title\n\n" + assert tokens.theMarkdown[-1] == "#! Novel Title\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._isFirst = True - theToken._text = "#! Note Title\n" + tokens._isNovel = False + tokens._isNote = True + tokens._isFirst = True + tokens._text = "#! Note Title\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Note Title", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "#! Note Title\n\n" + assert tokens.theMarkdown[-1] == "#! Note Title\n\n" # Header 1 # ======== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._isFirst = True - theToken._text = "# Novel Title\n" + tokens._isNovel = True + tokens._isNote = False + tokens._isFirst = True + tokens._text = "# Novel Title\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "# Novel Title\n\n" + assert tokens.theMarkdown[-1] == "# Novel Title\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._isFirst = True - theToken._text = "# Note Title\n" + tokens._isNovel = False + tokens._isNote = True + tokens._isFirst = True + tokens._text = "# Note Title\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Note Title", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "# Note Title\n\n" + assert tokens.theMarkdown[-1] == "# Note Title\n\n" # Header 2 # ======== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._text = "## Chapter One\n" + tokens._isNovel = True + tokens._isNote = False + tokens._text = "## Chapter One\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "## Chapter One\n\n" + assert tokens.theMarkdown[-1] == "## Chapter One\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._text = "## Heading 2\n" + tokens._isNovel = False + tokens._isNote = True + tokens._text = "## Heading 2\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "Heading 2", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "## Heading 2\n\n" + assert tokens.theMarkdown[-1] == "## Heading 2\n\n" # Header 3 # ======== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._text = "### Scene One\n" + tokens._isNovel = True + tokens._isNote = False + tokens._text = "### Scene One\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "### Scene One\n\n" + assert tokens.theMarkdown[-1] == "### Scene One\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._text = "### Heading 3\n" + tokens._isNovel = False + tokens._isNote = True + tokens._text = "### Heading 3\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD3, 1, "Heading 3", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "### Heading 3\n\n" + assert tokens.theMarkdown[-1] == "### Heading 3\n\n" # Header 4 # ======== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._text = "#### A Section\n" + tokens._isNovel = True + tokens._isNote = False + tokens._text = "#### A Section\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "#### A Section\n\n" + assert tokens.theMarkdown[-1] == "#### A Section\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._text = "#### Heading 4\n" + tokens._isNovel = False + tokens._isNote = True + tokens._text = "#### Heading 4\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD4, 1, "Heading 4", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "#### Heading 4\n\n" + assert tokens.theMarkdown[-1] == "#### Heading 4\n\n" # Title # ===== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._text = "#! Title\n" + tokens._isNovel = True + tokens._isNote = False + tokens._text = "#! Title\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_TITLE, 1, "Title", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "#! Title\n\n" + assert tokens.theMarkdown[-1] == "#! Title\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._text = "#! Title\n" + tokens._isNovel = False + tokens._isNote = True + tokens._text = "#! Title\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "#! Title\n\n" + assert tokens.theMarkdown[-1] == "#! Title\n\n" # Unnumbered # ========== # Story File - theToken._isNovel = True - theToken._isNote = False - theToken._text = "##! Prologue\n" + tokens._isNovel = True + tokens._isNote = False + tokens._text = "##! Prologue\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_UNNUM, 1, "Prologue", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "##! Prologue\n\n" + assert tokens.theMarkdown[-1] == "##! Prologue\n\n" # Note File - theToken._isNovel = False - theToken._isNote = True - theToken._text = "##! Prologue\n" + tokens._isNovel = False + tokens._isNote = True + tokens._text = "##! Prologue\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "Prologue", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "##! Prologue\n\n" + assert tokens.theMarkdown[-1] == "##! Prologue\n\n" # END Test testCoreToken_HeaderFormat @@ -426,67 +434,67 @@ def testCoreToken_HeaderFormat(mockGUI): @pytest.mark.core def testCoreToken_MetaFormat(mockGUI): """Test the tokenization of meta formats in the Tokenizer class.""" - theProject = NWProject() - theToken = BareTokenizer(theProject) - theToken.setKeepMarkdown(True) + project = NWProject() + tokens = BareTokenizer(project) + tokens.setKeepMarkdown(True) # Comment - theToken._text = "% A comment\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "% A comment\n" + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_COMMENT, 0, "A comment", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "\n" + assert tokens.theMarkdown[-1] == "\n" - theToken.setComments(True) - theToken.tokenizeText() - assert theToken.theMarkdown[-1] == "% A comment\n\n" + tokens.setComments(True) + tokens.tokenizeText() + assert tokens.theMarkdown[-1] == "% A comment\n\n" # Synopsis - theToken._text = "%synopsis: The synopsis\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "%synopsis: The synopsis\n" + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_SYNOPSIS, 0, "The synopsis", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - theToken._text = "% synopsis: The synopsis\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "% synopsis: The synopsis\n" + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_SYNOPSIS, 0, "The synopsis", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "\n" + assert tokens.theMarkdown[-1] == "\n" - theToken.setSynopsis(True) - theToken.tokenizeText() - assert theToken.theMarkdown[-1] == "% synopsis: The synopsis\n\n" + tokens.setSynopsis(True) + tokens.tokenizeText() + assert tokens.theMarkdown[-1] == "% synopsis: The synopsis\n\n" # Keyword - theToken._text = "@char: Bod\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "@char: Bod\n" + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_KEYWORD, 0, "char: Bod", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "\n" + assert tokens.theMarkdown[-1] == "\n" - theToken.setKeywords(True) - theToken.tokenizeText() - assert theToken.theMarkdown[-1] == "@char: Bod\n\n" + tokens.setKeywords(True) + tokens.tokenizeText() + assert tokens.theMarkdown[-1] == "@char: Bod\n\n" - theToken._text = "@pov: Bod\n@plot: Main\n@location: Europe\n" - theToken.tokenizeText() + tokens._text = "@pov: Bod\n@plot: Main\n@location: Europe\n" + tokens.tokenizeText() styTop = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG styMid = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG | Tokenizer.A_Z_TOPMRG styBtm = Tokenizer.A_NONE | Tokenizer.A_Z_TOPMRG - assert theToken._tokens == [ + assert tokens._tokens == [ (Tokenizer.T_KEYWORD, 0, "pov: Bod", None, styTop), (Tokenizer.T_KEYWORD, 0, "plot: Main", None, styMid), (Tokenizer.T_KEYWORD, 0, "location: Europe", None, styBtm), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "@pov: Bod\n@plot: Main\n@location: Europe\n\n" + assert tokens.theMarkdown[-1] == "@pov: Bod\n@plot: Main\n@location: Europe\n\n" # END Test testCoreToken_MetaFormat @@ -494,14 +502,14 @@ def testCoreToken_MetaFormat(mockGUI): @pytest.mark.core def testCoreToken_MarginFormat(mockGUI): """Test the tokenization of margin formats in the Tokenizer class.""" - theProject = NWProject() - theToken = BareTokenizer(theProject) - theToken.setKeepMarkdown(True) + project = NWProject() + tokens = BareTokenizer(project) + tokens.setKeepMarkdown(True) # Alignment and Indentation dblIndent = Tokenizer.A_IND_L | Tokenizer.A_IND_R rIndAlign = Tokenizer.A_RIGHT | Tokenizer.A_IND_R - theToken._text = ( + tokens._text = ( "Some regular text\n\n" "Some left-aligned text <<\n\n" ">> Some right-aligned text\n\n" @@ -511,8 +519,8 @@ def testCoreToken_MarginFormat(mockGUI): "> Double-indented block <\n\n" ">> Right-indent, right-aligned <\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_TEXT, 0, "Some regular text", [], Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), (Tokenizer.T_TEXT, 0, "Some left-aligned text", [], Tokenizer.A_LEFT), @@ -531,7 +539,7 @@ def testCoreToken_MarginFormat(mockGUI): (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == ( + assert tokens.theMarkdown[-1] == ( "Some regular text\n\n" "Some left-aligned text\n\n" "Some right-aligned text\n\n" @@ -545,121 +553,212 @@ def testCoreToken_MarginFormat(mockGUI): # END Test testCoreToken_MarginFormat +@pytest.mark.core +def testCoreToken_ExtractFormats(mockGUI): + """Test the extraction of formats in the Tokenizer class.""" + project = NWProject() + tokens = BareTokenizer(project) + tokens.setKeepMarkdown(True) + + # Markdown + # ======== + + # Plain bold + text, fmt = tokens._extractFormats("Text with **bold** in it.") + assert text == "Text with bold in it." + assert fmt == [(10, tokens.FMT_B_B), (14, tokens.FMT_B_E)] + + # Plain italics + text, fmt = tokens._extractFormats("Text with _italics_ in it.") + assert text == "Text with italics in it." + assert fmt == [(10, tokens.FMT_I_B), (17, tokens.FMT_I_E)] + + # Plain strikethrough + text, fmt = tokens._extractFormats("Text with ~~strikethrough~~ in it.") + assert text == "Text with strikethrough in it." + assert fmt == [(10, tokens.FMT_D_B), (23, tokens.FMT_D_E)] + + # Nested bold/italics + text, fmt = tokens._extractFormats("Text with **bold and _italics_** in it.") + assert text == "Text with bold and italics in it." + assert fmt == [ + (10, tokens.FMT_B_B), (19, tokens.FMT_I_B), (26, tokens.FMT_I_E), (26, tokens.FMT_B_E) + ] + + # Bold with overlapping italics + # Here, bold is ignored because it is not on word boundary + text, fmt = tokens._extractFormats("Text with **bold and overlapping _italics**_ in it.") + assert text == "Text with **bold and overlapping italics** in it." + assert fmt == [(33, tokens.FMT_I_B), (42, tokens.FMT_I_E)] + + # Shortcodes + # ========== + + # Plain bold + text, fmt = tokens._extractFormats("Text with [b]bold[/b] in it.") + assert text == "Text with bold in it." + assert fmt == [(10, tokens.FMT_B_B), (14, tokens.FMT_B_E)] + + # Plain italics + text, fmt = tokens._extractFormats("Text with [i]italics[/i] in it.") + assert text == "Text with italics in it." + assert fmt == [(10, tokens.FMT_I_B), (17, tokens.FMT_I_E)] + + # Plain strikethrough + text, fmt = tokens._extractFormats("Text with [s]strikethrough[/s] in it.") + assert text == "Text with strikethrough in it." + assert fmt == [(10, tokens.FMT_D_B), (23, tokens.FMT_D_E)] + + # Plain underline + text, fmt = tokens._extractFormats("Text with [u]underline[/u] in it.") + assert text == "Text with underline in it." + assert fmt == [(10, tokens.FMT_U_B), (19, tokens.FMT_U_E)] + + # Plain superscript + text, fmt = tokens._extractFormats("Text with super[sup]script[/sup] in it.") + assert text == "Text with superscript in it." + assert fmt == [(15, tokens.FMT_SUP_B), (21, tokens.FMT_SUP_E)] + + # Plain subscript + text, fmt = tokens._extractFormats("Text with sub[sub]script[/sub] in it.") + assert text == "Text with subscript in it." + assert fmt == [(13, tokens.FMT_SUB_B), (19, tokens.FMT_SUB_E)] + + # Nested bold/italics + text, fmt = tokens._extractFormats("Text with [b]bold and [i]italics[/i][/b] in it.") + assert text == "Text with bold and italics in it." + assert fmt == [ + (10, tokens.FMT_B_B), (19, tokens.FMT_I_B), (26, tokens.FMT_I_E), (26, tokens.FMT_B_E) + ] + + # Bold with overlapping italics + # With shortcodes, this works + text, fmt = tokens._extractFormats( + "Text with [b]bold and overlapping [i]italics[/b][/i] in it." + ) + assert text == "Text with bold and overlapping italics in it." + assert fmt == [ + (10, tokens.FMT_B_B), (31, tokens.FMT_I_B), (38, tokens.FMT_B_E), (38, tokens.FMT_I_E) + ] + +# END Test testCoreToken_ExtractFormats + + @pytest.mark.core def testCoreToken_TextFormat(mockGUI): """Test the tokenization of text formats in the Tokenizer class.""" - theProject = NWProject() - theToken = BareTokenizer(theProject) - theToken.setKeepMarkdown(True) + project = NWProject() + tokens = BareTokenizer(project) + tokens.setKeepMarkdown(True) # Text - theToken._text = "Some plain text\non two lines\n\n\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "Some plain text\non two lines\n\n\n" + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_TEXT, 0, "Some plain text", [], Tokenizer.A_NONE), (Tokenizer.T_TEXT, 0, "on two lines", [], Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "Some plain text\non two lines\n\n\n\n" + assert tokens.theMarkdown[-1] == "Some plain text\non two lines\n\n\n\n" - theToken.setBodyText(False) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.setBodyText(False) + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "\n\n\n" - theToken.setBodyText(True) + assert tokens.theMarkdown[-1] == "\n\n\n" + tokens.setBodyText(True) # Text Emphasis - theToken._text = "Some **bolded text** on this lines\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "Some **bolded text** on this lines\n" + tokens.tokenizeText() + assert tokens._tokens == [ ( Tokenizer.T_TEXT, 0, - "Some **bolded text** on this lines", + "Some bolded text on this lines", [ - [5, 2, Tokenizer.FMT_B_B], - [18, 2, Tokenizer.FMT_B_E], + (5, Tokenizer.FMT_B_B), + (16, Tokenizer.FMT_B_E), ], Tokenizer.A_NONE ), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "Some **bolded text** on this lines\n\n" + assert tokens.theMarkdown[-1] == "Some **bolded text** on this lines\n\n" - theToken._text = "Some _italic text_ on this lines\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "Some _italic text_ on this lines\n" + tokens.tokenizeText() + assert tokens._tokens == [ ( Tokenizer.T_TEXT, 0, - "Some _italic text_ on this lines", + "Some italic text on this lines", [ - [5, 1, Tokenizer.FMT_I_B], - [17, 1, Tokenizer.FMT_I_E], + (5, Tokenizer.FMT_I_B), + (16, Tokenizer.FMT_I_E), ], Tokenizer.A_NONE ), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "Some _italic text_ on this lines\n\n" + assert tokens.theMarkdown[-1] == "Some _italic text_ on this lines\n\n" - theToken._text = "Some **_bold italic text_** on this lines\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "Some **_bold italic text_** on this lines\n" + tokens.tokenizeText() + assert tokens._tokens == [ ( Tokenizer.T_TEXT, 0, - "Some **_bold italic text_** on this lines", + "Some bold italic text on this lines", [ - [5, 2, Tokenizer.FMT_B_B], - [7, 1, Tokenizer.FMT_I_B], - [24, 1, Tokenizer.FMT_I_E], - [25, 2, Tokenizer.FMT_B_E], + (5, Tokenizer.FMT_B_B), + (5, Tokenizer.FMT_I_B), + (21, Tokenizer.FMT_I_E), + (21, Tokenizer.FMT_B_E), ], Tokenizer.A_NONE ), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n" + assert tokens.theMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n" - theToken._text = "Some ~~strikethrough text~~ on this lines\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "Some ~~strikethrough text~~ on this lines\n" + tokens.tokenizeText() + assert tokens._tokens == [ ( Tokenizer.T_TEXT, 0, - "Some ~~strikethrough text~~ on this lines", + "Some strikethrough text on this lines", [ - [5, 2, Tokenizer.FMT_D_B], - [25, 2, Tokenizer.FMT_D_E], + (5, Tokenizer.FMT_D_B), + (23, Tokenizer.FMT_D_E), ], Tokenizer.A_NONE ), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n" + assert tokens.theMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n" - theToken._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" - theToken.tokenizeText() - assert theToken._tokens == [ + tokens._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" + tokens.tokenizeText() + assert tokens._tokens == [ ( Tokenizer.T_TEXT, 0, - "Some **nested bold and _italic_ and ~~strikethrough~~ text** here", + "Some nested bold and italic and strikethrough text here", [ - [5, 2, Tokenizer.FMT_B_B], - [23, 1, Tokenizer.FMT_I_B], - [30, 1, Tokenizer.FMT_I_E], - [36, 2, Tokenizer.FMT_D_B], - [51, 2, Tokenizer.FMT_D_E], - [58, 2, Tokenizer.FMT_B_E], + (5, Tokenizer.FMT_B_B), + (21, Tokenizer.FMT_I_B), + (27, Tokenizer.FMT_I_E), + (32, Tokenizer.FMT_D_B), + (45, Tokenizer.FMT_D_E), + (50, Tokenizer.FMT_B_E), ], Tokenizer.A_NONE ), (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] - assert theToken.theMarkdown[-1] == ( + assert tokens.theMarkdown[-1] == ( "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" ) @@ -669,10 +768,10 @@ def testCoreToken_TextFormat(mockGUI): @pytest.mark.core def testCoreToken_SpecialFormat(mockGUI): """Test the tokenization of special formats in the Tokenizer class.""" - theProject = NWProject() - theToken = BareTokenizer(theProject) + project = NWProject() + tokens = BareTokenizer(project) - theToken._isNovel = True + tokens._isNovel = True # New Page # ======== @@ -687,45 +786,45 @@ def testCoreToken_SpecialFormat(mockGUI): ] # Command wo/Space - theToken._isFirst = True - theToken._text = ( + tokens._isFirst = True + tokens._text = ( "# Title One\n\n" - "[NEWPAGE]\n\n" + "[newpage]\n\n" "# Title Two\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == correctResp + tokens.tokenizeText() + assert tokens._tokens == correctResp # Command w/Space - theToken._isFirst = True - theToken._text = ( + tokens._isFirst = True + tokens._text = ( "# Title One\n\n" - "[NEW PAGE]\n\n" + "[new page]\n\n" "# Title Two\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == correctResp + tokens.tokenizeText() + assert tokens._tokens == correctResp # Trailing Spaces - theToken._isFirst = True - theToken._text = ( + tokens._isFirst = True + tokens._text = ( "# Title One\n\n" - "[NEW PAGE] \t\n\n" + "[new page] \t\n\n" "# Title Two\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == correctResp + tokens.tokenizeText() + assert tokens._tokens == correctResp # Single Empty Paragraph # ====================== - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[VSPACE] \n\n" + "[vspace] \n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), @@ -739,13 +838,13 @@ def testCoreToken_SpecialFormat(mockGUI): # ========================= # One Skip - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[VSPACE:1] \n\n" + "[vspace:1] \n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), @@ -756,13 +855,13 @@ def testCoreToken_SpecialFormat(mockGUI): ] # Three Skips - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[VSPACE:3] \n\n" + "[vspace:3] \n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), @@ -775,13 +874,13 @@ def testCoreToken_SpecialFormat(mockGUI): ] # Malformed Command, Case 1 - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[VSPACE:3xa] \n\n" + "[vspace:3xa] \n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), @@ -791,13 +890,13 @@ def testCoreToken_SpecialFormat(mockGUI): ] # Malformed Command, Case 2 - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[VSPACE:3.5]\n\n" + "[vspace:3.5]\n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), @@ -807,13 +906,13 @@ def testCoreToken_SpecialFormat(mockGUI): ] # Malformed Command, Case 3 - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[VSPACE:-1]\n\n" + "[vspace:-1]\n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), @@ -826,14 +925,14 @@ def testCoreToken_SpecialFormat(mockGUI): # ============================== # Single Skip - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[NEW PAGE]\n\n" - "[VSPACE]\n\n" + "[new page]\n\n" + "[vspace]\n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), @@ -845,14 +944,14 @@ def testCoreToken_SpecialFormat(mockGUI): ] # Multiple Skip - theToken._text = ( + tokens._text = ( "# Title One\n\n" - "[NEW PAGE]\n\n" - "[VSPACE:3]\n\n" + "[new page]\n\n" + "[vspace:3]\n\n" "Some text to go here ...\n\n" ) - theToken.tokenizeText() - assert theToken._tokens == [ + tokens.tokenizeText() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), @@ -871,51 +970,51 @@ def testCoreToken_SpecialFormat(mockGUI): @pytest.mark.core def testCoreToken_ProcessHeaders(mockGUI): """Test the header and page parser of the Tokenizer class.""" - theProject = NWProject() - theProject.data.setLanguage("en") - theProject._loadProjectLocalisation() - theToken = BareTokenizer(theProject) + project = NWProject() + project.data.setLanguage("en") + project._loadProjectLocalisation() + tokens = BareTokenizer(project) # Nothing - theToken._text = "Some text ...\n" - assert theToken.doHeaders() is False - theToken._isNone = True - assert theToken.doHeaders() is False - theToken._isNone = False - assert theToken.doHeaders() is False - theToken._isNote = True - assert theToken.doHeaders() is False - theToken._isNote = False + tokens._text = "Some text ...\n" + assert tokens.doHeaders() is False + tokens._isNone = True + assert tokens.doHeaders() is False + tokens._isNone = False + assert tokens.doHeaders() is False + tokens._isNote = True + assert tokens.doHeaders() is False + tokens._isNote = False ## # Story FIles ## - theToken._isNone = False - theToken._isNote = False - theToken._isNovel = True + tokens._isNone = False + tokens._isNote = False + tokens._isNovel = True # Titles # ====== # H1: Title, First Page - assert theToken._isFirst is True - theToken._text = "# Part One\n" - theToken.setTitleFormat(f"T: {nwHeadFmt.TITLE}") - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + assert tokens._isFirst is True + tokens._text = "# Part One\n" + tokens.setTitleFormat(f"T: {nwHeadFmt.TITLE}") + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "T: Part One", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H1: Title, Not First Page - assert theToken._isFirst is False - theToken._text = "# Part One\n" - theToken.setTitleFormat(f"T: {nwHeadFmt.TITLE}") - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + assert tokens._isFirst is False + tokens._text = "# Part One\n" + tokens.setTitleFormat(f"T: {nwHeadFmt.TITLE}") + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD1, 1, "T: Part One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] @@ -924,52 +1023,52 @@ def testCoreToken_ProcessHeaders(mockGUI): # ======== # H2: Chapter - theToken._text = "## Chapter One\n" - theToken.setChapterFormat(f"C: {nwHeadFmt.TITLE}") - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "## Chapter One\n" + tokens.setChapterFormat(f"C: {nwHeadFmt.TITLE}") + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "C: Chapter One", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H2: Unnumbered Chapter - theToken._text = "##! Prologue\n" - theToken.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}") - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "##! Prologue\n" + tokens.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}") + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_UNNUM, 1, "U: Prologue", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H2: Chapter Word Number - theToken._text = "## Chapter\n" - theToken.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}") - theToken._hFormatter._chCount = 0 - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "## Chapter\n" + tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}") + tokens._hFormatter._chCount = 0 + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H2: Chapter Roman Number Upper Case - theToken._text = "## Chapter\n" - theToken.setChapterFormat(f"Chapter {nwHeadFmt.CH_ROMU}") - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "## Chapter\n" + tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_ROMU}") + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "Chapter II", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H2: Chapter Roman Number Lower Case - theToken._text = "## Chapter\n" - theToken.setChapterFormat(f"Chapter {nwHeadFmt.CH_ROML}") - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "## Chapter\n" + tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_ROML}") + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD2, 1, "Chapter iii", None, Tokenizer.A_PBB), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] @@ -978,89 +1077,89 @@ def testCoreToken_ProcessHeaders(mockGUI): # ====== # H3: Scene w/Title - theToken._text = "### Scene One\n" - theToken.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False) - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### Scene One\n" + tokens.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False) + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD3, 1, "S: Scene One", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene Hidden wo/Format - theToken._text = "### Scene One\n" - theToken.setSceneFormat("", True) - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### Scene One\n" + tokens.setSceneFormat("", True) + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene wo/Format, first - theToken._text = "### Scene One\n" - theToken.setSceneFormat("", False) - theToken._firstScene = True - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### Scene One\n" + tokens.setSceneFormat("", False) + tokens._firstScene = True + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene wo/Format, not first - theToken._text = "### Scene One\n" - theToken.setSceneFormat("", False) - theToken._firstScene = False - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### Scene One\n" + tokens.setSceneFormat("", False) + tokens._firstScene = False + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene Separator, first - theToken._text = "### Scene One\n" - theToken.setSceneFormat("* * *", False) - theToken._firstScene = True - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### Scene One\n" + tokens.setSceneFormat("* * *", False) + tokens._firstScene = True + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene Separator, not first - theToken._text = "### Scene One\n" - theToken.setSceneFormat("* * *", False) - theToken._firstScene = False - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### Scene One\n" + tokens.setSceneFormat("* * *", False) + tokens._firstScene = False + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene w/Absolute Number - theToken._text = "### A Scene\n" - theToken.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False) - theToken._hFormatter._scAbsCount = 0 - theToken._hFormatter._scChCount = 0 - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### A Scene\n" + tokens.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False) + tokens._hFormatter._scAbsCount = 0 + tokens._hFormatter._scChCount = 0 + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD3, 1, "Scene 1", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H3: Scene w/Chapter Number - theToken._text = "### A Scene\n" - theToken.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False) - theToken._hFormatter._scAbsCount = 0 - theToken._hFormatter._scChCount = 1 - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "### A Scene\n" + tokens.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False) + tokens._hFormatter._scAbsCount = 0 + tokens._hFormatter._scChCount = 1 + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD3, 1, "Scene 3.2", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] @@ -1069,51 +1168,51 @@ def testCoreToken_ProcessHeaders(mockGUI): # ======== # H4: Section Hidden wo/Format - theToken._text = "#### A Section\n" - theToken.setSectionFormat(r"", True) - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "#### A Section\n" + tokens.setSectionFormat(r"", True) + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H4: Section Visible wo/Format - theToken._text = "#### A Section\n" - theToken.setSectionFormat("", False) - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "#### A Section\n" + tokens.setSectionFormat("", False) + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H4: Section w/Format - theToken._text = "#### A Section\n" - theToken.setSectionFormat(f"X: {nwHeadFmt.TITLE}", False) - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "#### A Section\n" + tokens.setSectionFormat(f"X: {nwHeadFmt.TITLE}", False) + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_HEAD4, 1, "X: A Section", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # H4: Section Separator - theToken._text = "#### A Section\n" - theToken.setSectionFormat("* * *", False) - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._tokens == [ + tokens._text = "#### A Section\n" + tokens.setSectionFormat("* * *", False) + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._tokens == [ (Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE), (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Check the first scene detector - assert theToken._firstScene is False - theToken._firstScene = True - theToken._text = "Some text ...\n" - theToken.tokenizeText() - theToken.doHeaders() - assert theToken._firstScene is False + assert tokens._firstScene is False + tokens._firstScene = True + tokens._text = "Some text ...\n" + tokens.tokenizeText() + tokens.doHeaders() + assert tokens._firstScene is False # END Test testCoreToken_ProcessHeaders diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py index 06658a85..204f1ac1 100644 --- a/tests/test_core/test_core_tomd.py +++ b/tests/test_core/test_core_tomd.py @@ -81,8 +81,8 @@ def testCoreToMarkdown_ConvertFormat(mockGUI): # Paragraphs # ========== - # Text for GitHub Markdown - theMD.setGitHubMarkdown() + # Text for Extended Markdown + theMD.setExtendedMarkdown() theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" theMD.tokenizeText() theMD.doConvert() diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 19962185..24be4883 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -53,53 +53,53 @@ def xmlToText(xElem): @pytest.mark.core def testCoreToOdt_Init(mockGUI): """Test initialisation of the ODT document.""" - theProject = NWProject() + project = NWProject() # Flat Doc # ======== - theDoc = ToOdt(theProject, isFlat=True) - theDoc.initDocument() + odt = ToOdt(project, isFlat=True) + odt.initDocument() # Document XML - assert theDoc._dFlat.tag == _mkTag("office", "document") - assert theDoc._dCont.tag == "" - assert theDoc._dMeta.tag == "" - assert theDoc._dStyl.tag == "" + assert odt._dFlat.tag == _mkTag("office", "document") + assert odt._dCont.tag == "" + assert odt._dMeta.tag == "" + assert odt._dStyl.tag == "" # Content XML - assert theDoc._xMeta.tag == _mkTag("office", "meta") - assert theDoc._xFont.tag == _mkTag("office", "font-face-decls") - assert theDoc._xFnt2.tag == "" - assert theDoc._xStyl.tag == _mkTag("office", "styles") - assert theDoc._xAuto.tag == _mkTag("office", "automatic-styles") - assert theDoc._xAut2.tag == "" - assert theDoc._xMast.tag == _mkTag("office", "master-styles") - assert theDoc._xBody.tag == _mkTag("office", "body") - assert theDoc._xText.tag == _mkTag("office", "text") + assert odt._xMeta.tag == _mkTag("office", "meta") + assert odt._xFont.tag == _mkTag("office", "font-face-decls") + assert odt._xFnt2.tag == "" + assert odt._xStyl.tag == _mkTag("office", "styles") + assert odt._xAuto.tag == _mkTag("office", "automatic-styles") + assert odt._xAut2.tag == "" + assert odt._xMast.tag == _mkTag("office", "master-styles") + assert odt._xBody.tag == _mkTag("office", "body") + assert odt._xText.tag == _mkTag("office", "text") # ODT Doc # ======= - theDoc = ToOdt(theProject, isFlat=False) - theDoc.initDocument() + odt = ToOdt(project, isFlat=False) + odt.initDocument() # Document XML - assert theDoc._dFlat.tag == "" - assert theDoc._dCont.tag == _mkTag("office", "document-content") - assert theDoc._dMeta.tag == _mkTag("office", "document-meta") - assert theDoc._dStyl.tag == _mkTag("office", "document-styles") + assert odt._dFlat.tag == "" + assert odt._dCont.tag == _mkTag("office", "document-content") + assert odt._dMeta.tag == _mkTag("office", "document-meta") + assert odt._dStyl.tag == _mkTag("office", "document-styles") # Content XML - assert theDoc._xMeta.tag == _mkTag("office", "meta") - assert theDoc._xFont.tag == _mkTag("office", "font-face-decls") - assert theDoc._xFnt2.tag == _mkTag("office", "font-face-decls") - assert theDoc._xStyl.tag == _mkTag("office", "styles") - assert theDoc._xAuto.tag == _mkTag("office", "automatic-styles") - assert theDoc._xAut2.tag == _mkTag("office", "automatic-styles") - assert theDoc._xMast.tag == _mkTag("office", "master-styles") - assert theDoc._xBody.tag == _mkTag("office", "body") - assert theDoc._xText.tag == _mkTag("office", "text") + assert odt._xMeta.tag == _mkTag("office", "meta") + assert odt._xFont.tag == _mkTag("office", "font-face-decls") + assert odt._xFnt2.tag == _mkTag("office", "font-face-decls") + assert odt._xStyl.tag == _mkTag("office", "styles") + assert odt._xAuto.tag == _mkTag("office", "automatic-styles") + assert odt._xAut2.tag == _mkTag("office", "automatic-styles") + assert odt._xMast.tag == _mkTag("office", "master-styles") + assert odt._xBody.tag == _mkTag("office", "body") + assert odt._xText.tag == _mkTag("office", "text") # END Test testCoreToOdt_Init @@ -108,147 +108,142 @@ def testCoreToOdt_Init(mockGUI): @pytest.mark.core def testCoreToOdt_TextFormatting(mockGUI): """Test formatting of paragraphs.""" - theProject = NWProject() - theDoc = ToOdt(theProject, isFlat=True) + project = NWProject() + odt = ToOdt(project, isFlat=True) - theDoc.initDocument() - assert xmlToText(theDoc._xText) == "" + odt.initDocument() + assert xmlToText(odt._xText) == "" # Paragraph Style # =============== oStyle = ODTParagraphStyle() - assert theDoc._paraStyle("stuff", oStyle) == "Standard" - assert theDoc._paraStyle("Text_20_body", oStyle) == "Text_20_body" + assert odt._paraStyle("stuff", oStyle) == "Standard" + assert odt._paraStyle("Text_20_body", oStyle) == "Text_20_body" # Create new para style oStyle.setTextAlign("center") - assert theDoc._paraStyle("Text_20_body", oStyle) == "P1" + assert odt._paraStyle("Text_20_body", oStyle) == "P1" # Return the same style on second call - assert theDoc._paraStyle("Text_20_body", oStyle) == "P1" + assert odt._paraStyle("Text_20_body", oStyle) == "P1" - assert list(theDoc._mainPara.keys()) == [ + assert list(odt._mainPara.keys()) == [ "Text_20_body", "Text_20_Meta", "Title", "Separator", "Heading_20_1", "Heading_20_2", "Heading_20_3", "Heading_20_4", "Header", ] theKey = "071d6b2e4764749f8c78d3c1ab9099fa04c07d2d53fd3de61eb1bdf1cb4845c3" - assert theDoc._autoPara[theKey][0] == "P1" - assert isinstance(theDoc._autoPara[theKey][1], ODTParagraphStyle) + assert odt._autoPara[theKey][0] == "P1" + assert isinstance(odt._autoPara[theKey][1], ODTParagraphStyle) # Paragraph Formatting # ==================== oStyle = ODTParagraphStyle() # No Text - theDoc.initDocument() - theDoc._addTextPar("Standard", oStyle, "") - assert xmlToText(theDoc._xText) == ( + odt.initDocument() + odt._addTextPar("Standard", oStyle, "") + assert xmlToText(odt._xText) == ( '' '' '' ) # No Format - theDoc.initDocument() - theDoc._addTextPar("Standard", oStyle, "Hello World") - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.initDocument() + odt._addTextPar("Standard", oStyle, "Hello World") + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Hello World' '' ) # Heading Level None - theDoc.initDocument() - theDoc._addTextPar("Standard", oStyle, "Hello World", isHead=True) - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.initDocument() + odt._addTextPar("Standard", oStyle, "Hello World", isHead=True) + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Hello World' '' ) # Heading Level 1 - theDoc.initDocument() - theDoc._addTextPar("Standard", oStyle, "Hello World", isHead=True, oLevel="1") - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.initDocument() + odt._addTextPar("Standard", oStyle, "Hello World", isHead=True, oLevel="1") + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Hello World' '' ) # Formatted Text - theDoc.initDocument() - theTxt = "A **few** _words_ from ~~our~~ sponsor" - theFmt = " _B b_ I i _S s_ " - theDoc._addTextPar("Standard", oStyle, theTxt, tFmt=theFmt) - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.initDocument() + text = "A bold word" + fmt = [(2, odt.FMT_B_B), (6, odt.FMT_B_E)] + odt._addTextPar("Standard", oStyle, text, tFmt=fmt) + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' - 'A few ' - 'words from ' - 'our sponsor' + 'A bold ' + 'word' '' ) # Incorrectly Formatted Text - theDoc.initDocument() - theTxt = "A **few** _wordsXXX" - theFmt = " _b b_ I XXX" - theDoc._addTextPar("Standard", oStyle, theTxt, tFmt=theFmt) - assert theDoc.errData == ["Unknown format tag encountered"] - assert xmlToText(theDoc._xText) == ( + odt.initDocument() + text = "A few words" + fmt = [(2, odt.FMT_B_B), (5, odt.FMT_B_E), (7, 99999)] + odt._addTextPar("Standard", oStyle, text, tFmt=fmt) + assert odt.errData == ["Unknown format tag encountered"] + assert xmlToText(odt._xText) == ( '' - '' - 'A few words' - '' + 'A few ' + 'words' '' ) - # Formatted Text - theDoc.initDocument() - theTxt = "Hello\n\tWorld" - theFmt = " " - theDoc._addTextPar("Standard", oStyle, theTxt, tFmt=theFmt) - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + # Unclosed format + odt.initDocument() + text = "A bold word" + fmt = [(2, odt.FMT_B_B)] + odt._addTextPar("Standard", oStyle, text, tFmt=fmt) + assert odt.errData == [] + assert xmlToText(odt._xText) == ( + '' + 'A ' + 'bold word' + '' + ) + + # Tabs and Breaks + odt.initDocument() + text = "Hello\n\tWorld" + fmt = [] + odt._addTextPar("Standard", oStyle, text, tFmt=fmt) + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'HelloWorld' '' ) - # Test for issue #1412 - # ==================== - # See: https://github.com/vkbo/novelWriter/issues/1412 - - theDoc.initDocument() - theTxt = "Test text \\**_bold_** and more." - theFmt = " I i " - theDoc._addTextPar("Standard", oStyle, theTxt, tFmt=theFmt) - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( - '' - 'Test text **' - 'bold** and more.' - '' - ) - # END Test testCoreToOdt_TextFormatting @pytest.mark.core def testCoreToOdt_Convert(mockGUI): """Test the converter of the ToOdt class.""" - theProject = NWProject() - theDoc = ToOdt(theProject, isFlat=True) + project = NWProject() + odt = ToOdt(project, isFlat=True) - theDoc._isNovel = True + odt._isNovel = True def getStyle(styleName): - for aSet in theDoc._autoPara.values(): + for aSet in odt._autoPara.values(): if aSet[0] == styleName: return aSet[1] return None @@ -257,78 +252,78 @@ def testCoreToOdt_Convert(mockGUI): # ======= # Header 1 - theDoc._text = "# Title\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "# Title\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Title' '' ) # Header 2 - theDoc._text = "## Chapter\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "## Chapter\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Chapter' '' ) # Header 3 - theDoc._text = "### Scene\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "### Scene\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Scene' '' ) # Header 4 - theDoc._text = "#### Section\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "#### Section\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Section' '' ) # Title - theDoc._text = "#! Title\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "#! Title\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Title' '' ) # Unnumbered chapter - theDoc._text = "##! Prologue\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "##! Prologue\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Prologue' '' @@ -337,14 +332,14 @@ def testCoreToOdt_Convert(mockGUI): # Paragraphs # ========== - # Nested Text - theDoc._text = "Some ~~nested **bold** and _italics_ text~~ text." - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + # Nested Markdown Text + odt._text = "Some ~~nested **bold** and _italics_ text~~ text." + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Some ' 'nested ' @@ -355,59 +350,94 @@ def testCoreToOdt_Convert(mockGUI): '' ) + # Nested Shortcode Text, Emphasis + odt._text = "Some [s]nested [b]bold[/b] [u]and[/u] [i]italics[/i] text[/s] text." + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( + '' + 'Some ' + 'nested ' + 'bold' + ' ' + 'and' + ' ' + 'italics' + ' text text.' + '' + ) + + # Nested Shortcode Text, Super/Subscript + odt._text = "Some super[sup]script[/sup] and sub[sub]script[/sub] text." + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( + '' + 'Some ' + 'superscript and ' + 'subscript text.' + '' + ) + # Hard Break - theDoc._text = "Some text.\nNext line\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "Some text.\nNext line\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Some text.Next line' '' ) # Tab - theDoc._text = "\tItem 1\tItem 2\n" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "\tItem 1\tItem 2\n" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Item 1Item 2' '' ) # Tab in Format - theDoc._text = "Some **bold\ttext**" - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "Some **bold\ttext**" + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' - 'Some ' + 'Some ' 'boldtext' '' ) # Multiple Spaces - theDoc._text = ( + odt._text = ( "### Scene\n\n" "Hello World\n\n" "Hello World\n\n" "Hello World\n\n" ) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Scene' 'Hello World' @@ -417,42 +447,42 @@ def testCoreToOdt_Convert(mockGUI): ) # Synopsis, Comment, Keywords - theDoc._text = ( + odt._text = ( "### Scene\n\n" "@pov: Jane\n\n" "% synopsis: So it begins\n\n" "% a plain comment\n\n" ) - theDoc.setSynopsis(True) - theDoc.setComments(True) - theDoc.setKeywords(True) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.setSynopsis(True) + odt.setComments(True) + odt.setKeywords(True) + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Scene' - '' + '' 'Point of View: Jane' - '' + '' 'Synopsis: So it begins' - '' + '' 'Comment: a plain comment' '' ) # Scene Separator - theDoc._text = "### Scene One\n\nText\n\n### Scene Two\n\nText" - theDoc.setSceneFormat("* * *", False) - theDoc.tokenizeText() - theDoc.doHeaders() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "### Scene One\n\nText\n\n### Scene Two\n\nText" + odt.setSceneFormat("* * *", False) + odt.tokenizeText() + odt.doHeaders() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' '* * *' 'Text' @@ -462,15 +492,15 @@ def testCoreToOdt_Convert(mockGUI): ) # Scene Break - theDoc._text = "### Scene One\n\nText\n\n### Scene Two\n\nText" - theDoc.setSceneFormat("", False) - theDoc.tokenizeText() - theDoc.doHeaders() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt._text = "### Scene One\n\nText\n\n### Scene Two\n\nText" + odt.setSceneFormat("", False) + odt.tokenizeText() + odt.doHeaders() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' '' 'Text' @@ -480,7 +510,7 @@ def testCoreToOdt_Convert(mockGUI): ) # Paragraph Styles - theDoc._text = ( + odt._text = ( "### Scene\n\n" "@pov: Jane\n" "@char: John\n" @@ -491,20 +521,20 @@ def testCoreToOdt_Convert(mockGUI): "> Left indent\n\n" "Right indent <\n\n" ) - theDoc.setKeywords(True) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.setKeywords(True) + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Scene' - '' + '' 'Point of View: Jane' - '' + '' 'Characters: John' - '' + '' 'Plot: Main' 'Right align' 'Left Align' @@ -513,28 +543,28 @@ def testCoreToOdt_Convert(mockGUI): 'Right indent' '' ) - assert getStyle("P3")._pAttr["margin-bottom"] == ["fo", "0.000cm"] - assert getStyle("P4")._pAttr["margin-bottom"] == ["fo", "0.000cm"] - assert getStyle("P4")._pAttr["margin-top"] == ["fo", "0.000cm"] - assert getStyle("P5")._pAttr["text-align"] == ["fo", "right"] - assert getStyle("P6")._pAttr["text-align"] == ["fo", "center"] - assert getStyle("P7")._pAttr["margin-left"] == ["fo", "1.693cm"] - assert getStyle("P8")._pAttr["margin-right"] == ["fo", "1.693cm"] + assert getStyle("P3")._pAttr["margin-bottom"] == ["fo", "0.000cm"] # type: ignore + assert getStyle("P4")._pAttr["margin-bottom"] == ["fo", "0.000cm"] # type: ignore + assert getStyle("P4")._pAttr["margin-top"] == ["fo", "0.000cm"] # type: ignore + assert getStyle("P5")._pAttr["text-align"] == ["fo", "right"] # type: ignore + assert getStyle("P6")._pAttr["text-align"] == ["fo", "center"] # type: ignore + assert getStyle("P7")._pAttr["margin-left"] == ["fo", "1.693cm"] # type: ignore + assert getStyle("P8")._pAttr["margin-right"] == ["fo", "1.693cm"] # type: ignore # Justified - theDoc._text = ( + odt._text = ( "### Scene\n\n" "Regular paragraph\n\n" "with\nbreak\n\n" "Left Align <<\n\n" ) - theDoc.setJustify(True) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.setJustify(True) + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Scene' 'Regular paragraph' @@ -542,21 +572,21 @@ def testCoreToOdt_Convert(mockGUI): 'Left Align' '' ) - assert getStyle("P9")._pAttr["text-align"] == ["fo", "left"] + assert getStyle("P9")._pAttr["text-align"] == ["fo", "left"] # type: ignore # Page Breaks - theDoc._text = ( + odt._text = ( "## Chapter One\n\n" "Text\n\n" "## Chapter Two\n\n" "Text\n\n" ) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() - assert theDoc.errData == [] - assert xmlToText(theDoc._xText) == ( + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( '' 'Chapter One' 'Text' @@ -565,6 +595,23 @@ def testCoreToOdt_Convert(mockGUI): '' ) + # Test for issue #1412 + # ==================== + # See: https://github.com/vkbo/novelWriter/issues/1412 + + odt._text = "Test text \\**_bold_** and more." + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() + assert odt.errData == [] + assert xmlToText(odt._xText) == ( + '' + 'Test text **' + 'bold** and more.' + '' + ) + # END Test testCoreToOdt_Convert @@ -626,37 +673,45 @@ def testCoreToOdt_ConvertDirect(mockGUI): @pytest.mark.core def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): """Test the document save functions.""" - theProject = NWProject() - theProject.data.setAuthor("Jane Smith") - theProject.data.setName("Test Project") - theProject.data.setSaveCount(1234) - theProject.data.setEditTime(3674096) + project = NWProject() + project.data.setAuthor("Jane Smith") + project.data.setName("Test Project") + project.data.setSaveCount(1234) + project.data.setEditTime(3674096) - theDoc = ToOdt(theProject, isFlat=True) - theDoc._isNovel = True - theDoc._dLanguage = "" - theDoc.setLanguage(None) - assert theDoc._dLanguage == "" - theDoc.setLanguage("nb_NO") - assert theDoc._dLanguage == "nb" - theDoc.setColourHeaders(True) + odt = ToOdt(project, isFlat=True) + odt._isNovel = True + odt._dLanguage = "" + odt.setLanguage(None) # type: ignore + assert odt._dLanguage == "" + odt.setLanguage("nb_NO") + assert odt._dLanguage == "nb" + odt.setColourHeaders(True) - theDoc._text = ( + odt.setPageLayout(148, 210, 20, 18, 17, 15) + assert odt._mDocWidth == "14.800cm" + assert odt._mDocHeight == "21.000cm" + assert odt._mDocTop == "2.000cm" + assert odt._mDocBtm == "1.800cm" + assert odt._mDocLeft == "1.700cm" + assert odt._mDocRight == "1.500cm" + + odt._text = ( "## Chapter One\n\n" "Text\n\n" "## Chapter Two\n\n" "Text\n\n" ) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() flatFile = fncPath / "document.fodt" testFile = tstPaths.outDir / "coreToOdt_SaveFlat_document.fodt" compFile = tstPaths.refDir / "coreToOdt_SaveFlat_document.fodt" - theDoc.saveFlatXML(flatFile) + odt.saveFlatXML(flatFile) assert flatFile.exists() copyfile(flatFile, testFile) @@ -668,29 +723,29 @@ def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): @pytest.mark.core def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): """Test the document save functions.""" - theProject = NWProject() - theProject.data.setAuthor("Jane Smith") - theProject.data.setName("Test Project") - theProject.data.setSaveCount(1234) - theProject.data.setEditTime(3674096) + project = NWProject() + project.data.setAuthor("Jane Smith") + project.data.setName("Test Project") + project.data.setSaveCount(1234) + project.data.setEditTime(3674096) - theDoc = ToOdt(theProject, isFlat=False) - theDoc._isNovel = True + odt = ToOdt(project, isFlat=False) + odt._isNovel = True - theDoc._text = ( + odt._text = ( "## Chapter One\n\n" "Text\n\n" "## Chapter Two\n\n" "Text\n\n" ) - theDoc.tokenizeText() - theDoc.initDocument() - theDoc.doConvert() - theDoc.closeDocument() + odt.tokenizeText() + odt.initDocument() + odt.doConvert() + odt.closeDocument() fullFile = fncPath / "document.odt" - theDoc.saveOpenDocText(fullFile) + odt.saveOpenDocText(fullFile) assert fullFile.exists() assert zipfile.is_zipfile(fullFile) @@ -745,26 +800,22 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): @pytest.mark.core def testCoreToOdt_Format(mockGUI): """Test the formatters for the ToOdt class.""" - theProject = NWProject() - theDoc = ToOdt(theProject, isFlat=True) + project = NWProject() + odt = ToOdt(project, isFlat=True) - assert theDoc._formatSynopsis("synopsis text") == ( - "**Synopsis:** synopsis text", - "_B b_ " + assert odt._formatSynopsis("synopsis text") == ( + "Synopsis: synopsis text", [(0, ToOdt.FMT_B_B), (9, ToOdt.FMT_B_E)] ) - assert theDoc._formatComments("comment text") == ( - "**Comment:** comment text", - "_B b_ " + assert odt._formatComments("comment text") == ( + "Comment: comment text", [(0, ToOdt.FMT_B_B), (8, ToOdt.FMT_B_E)] ) - assert theDoc._formatKeywords("") == ("", "") - assert theDoc._formatKeywords("tag: Jane") == ( - "**Tag:** Jane", - "_B b_ " + assert odt._formatKeywords("") == ("", []) + assert odt._formatKeywords("tag: Jane") == ( + "Tag: Jane", [(0, ToOdt.FMT_B_B), (4, ToOdt.FMT_B_E)] ) - assert theDoc._formatKeywords("char: Bod, Jane") == ( - "**Characters:** Bod, Jane", - "_B b_ " + assert odt._formatKeywords("char: Bod, Jane") == ( + "Characters: Bod, Jane", [(0, ToOdt.FMT_B_B), (11, ToOdt.FMT_B_E)] ) # END Test testCoreToOdt_Format @@ -1016,6 +1067,17 @@ def testCoreToOdt_ODTTextStyle(): txtStyle.setFontStyle("stuff") assert txtStyle._tAttr["font-style"] == ["fo", None] + # Text Position + assert txtStyle._tAttr["text-position"] == ["style", None] + txtStyle.setTextPosition("stuff") + assert txtStyle._tAttr["text-position"] == ["style", None] + txtStyle.setTextPosition("super") + assert txtStyle._tAttr["text-position"] == ["style", "super 58%"] + txtStyle.setTextPosition("sub") + assert txtStyle._tAttr["text-position"] == ["style", "sub 58%"] + txtStyle.setTextPosition("stuff") + assert txtStyle._tAttr["text-position"] == ["style", None] + # Line Through Style assert txtStyle._tAttr["text-line-through-style"] == ["style", None] txtStyle.setStrikeStyle("stuff") @@ -1031,8 +1093,8 @@ def testCoreToOdt_ODTTextStyle(): assert txtStyle._tAttr["text-line-through-type"] == ["style", None] txtStyle.setStrikeType("stuff") assert txtStyle._tAttr["text-line-through-type"] == ["style", None] - txtStyle.setStrikeType("none") - assert txtStyle._tAttr["text-line-through-type"] == ["style", "none"] + txtStyle.setStrikeType("none") # Deprecated in ODF 1.3 + assert txtStyle._tAttr["text-line-through-type"] == ["style", None] txtStyle.setStrikeType("single") assert txtStyle._tAttr["text-line-through-type"] == ["style", "single"] txtStyle.setStrikeType("double") @@ -1040,18 +1102,49 @@ def testCoreToOdt_ODTTextStyle(): txtStyle.setStrikeType("stuff") assert txtStyle._tAttr["text-line-through-type"] == ["style", None] + # Underline Style + assert txtStyle._tAttr["text-underline-style"] == ["style", None] + txtStyle.setUnderlineStyle("stuff") + assert txtStyle._tAttr["text-underline-style"] == ["style", None] + txtStyle.setUnderlineStyle("none") + assert txtStyle._tAttr["text-underline-style"] == ["style", "none"] + txtStyle.setUnderlineStyle("solid") + assert txtStyle._tAttr["text-underline-style"] == ["style", "solid"] + txtStyle.setUnderlineStyle("stuff") + assert txtStyle._tAttr["text-underline-style"] == ["style", None] + + # Underline Width + assert txtStyle._tAttr["text-underline-width"] == ["style", None] + txtStyle.setUnderlineWidth("stuff") + assert txtStyle._tAttr["text-underline-width"] == ["style", None] + txtStyle.setUnderlineWidth("auto") + assert txtStyle._tAttr["text-underline-width"] == ["style", "auto"] + txtStyle.setUnderlineWidth("stuff") + assert txtStyle._tAttr["text-underline-width"] == ["style", None] + + # Underline Colour + assert txtStyle._tAttr["text-underline-color"] == ["style", None] + txtStyle.setUnderlineColour("stuff") + assert txtStyle._tAttr["text-underline-color"] == ["style", None] + txtStyle.setUnderlineColour("font-color") + assert txtStyle._tAttr["text-underline-color"] == ["style", "font-color"] + txtStyle.setUnderlineColour("stuff") + assert txtStyle._tAttr["text-underline-color"] == ["style", None] + # Pack XML # ======== txtStyle.setFontWeight("bold") txtStyle.setFontStyle("italic") + txtStyle.setTextPosition("super") txtStyle.setStrikeStyle("solid") txtStyle.setStrikeType("single") xStyle = ET.Element("test") txtStyle.packXML(xStyle, "test") assert xmlToText(xStyle) == ( '' + 'fo:font-weight="bold" fo:font-style="italic" style:text-position="super 58%" ' + 'style:text-line-through-style="solid" style:text-line-through-type="single" />' + '' ) # END Test testCoreToOdt_ODTTextStyle diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 5fe1ae49..e66e5dc6 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -600,15 +600,15 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.mainMenu.aInsNewPage.activate(QAction.Trigger) - assert nwGUI.docEditor.getText() == "[NEW PAGE]\n### Stuff\n" + assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n" nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.mainMenu.aInsVSpaceS.activate(QAction.Trigger) - assert nwGUI.docEditor.getText() == "[VSPACE]\n### Stuff\n" + assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n" nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.mainMenu.aInsVSpaceM.activate(QAction.Trigger) - assert nwGUI.docEditor.getText() == "[VSPACE:2]\n### Stuff\n" + assert nwGUI.docEditor.getText() == "[vspace:2]\n### Stuff\n" nwGUI.docEditor.clear()