From 6b6c517b1ff0d68cbb4c19bd6354767cc74440bd Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 6 Jun 2023 00:35:57 +0200 Subject: [PATCH] Add annotations to tokenizer, html and markdown classes --- novelwriter/core/tohtml.py | 123 +++++++++++++-------------- novelwriter/core/tokenizer.py | 153 +++++++++++++++++++--------------- novelwriter/core/tomd.py | 67 +++++++-------- 3 files changed, 177 insertions(+), 166 deletions(-) diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index 51a9ef44..323b617c 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -22,11 +22,15 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import logging +from pathlib import Path + from novelwriter import CONFIG from novelwriter.constants import nwKeyWords, nwLabels, nwHtmlUnicode +from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer, stripEscape logger = logging.getLogger(__name__) @@ -38,8 +42,8 @@ class ToHtml(Tokenizer): M_EXPORT = 1 # Tweak output for saving to HTML or printing M_EBOOK = 2 # Tweak output for converting to epub - def __init__(self, theProject): - super().__init__(theProject) + def __init__(self, project: NWProject): + super().__init__(project) self._genMode = self.M_EXPORT self._cssStyles = True @@ -63,7 +67,7 @@ class ToHtml(Tokenizer): # Setters ## - def setPreview(self, doComments, doSynopsis): + def setPreview(self, doComments: bool, doSynopsis: bool): """If we're using this class to generate markdown preview, we need to make a few changes to formatting, which is managed by these flags. @@ -74,14 +78,14 @@ class ToHtml(Tokenizer): self._doSynopsis = doSynopsis return - def setStyles(self, cssStyles): - """Enable/disable CSS styling. Some elements may still have + def setStyles(self, cssStyles: bool): + """Enable or disable CSS styling. Some elements may still have class tags. """ self._cssStyles = cssStyles return - def setReplaceUnicode(self, doReplace): + def setReplaceUnicode(self, doReplace: bool): """Set the translation map to either minimal or full unicode for html entities replacement. """ @@ -92,16 +96,14 @@ class ToHtml(Tokenizer): if doReplace: # Extend to all relevant Unicode characters self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H)) - return ## # Class Methods ## - def getFullResultSize(self): - """Return the size of the full HTML result. - """ + def getFullResultSize(self) -> int: + """Return the size of the full HTML result.""" return sum([len(x) for x in self._fullHTML]) def doPreProcessing(self): @@ -114,7 +116,7 @@ class ToHtml(Tokenizer): def doConvert(self): """Convert the list of text tokens into a HTML document saved - to theResult. + to _result. """ if self._genMode == self.M_PREVIEW: htmlTags = { # HTML4 + CSS2 (for Qt) @@ -290,7 +292,7 @@ class ToHtml(Tokenizer): return - def saveHTML5(self, savePath): + def saveHTML5(self, savePath: str | Path): """Save the data to an .html file. """ with open(savePath, mode="w", encoding="utf-8") as outFile: @@ -324,9 +326,8 @@ class ToHtml(Tokenizer): return - def replaceTabs(self, nSpaces=8, spaceChar=" "): - """Replace tabs with spaces in the html. - """ + def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " "): + """Replace tabs with spaces in the html.""" htmlText = [] tabSpace = spaceChar*nSpaces for aLine in self._fullHTML: @@ -335,20 +336,19 @@ class ToHtml(Tokenizer): self._fullHTML = htmlText return - def getStyleSheet(self): - """Generate a stylesheet appropriate for the current settings. - """ - theStyles = [] + def getStyleSheet(self) -> list: + """Generate a stylesheet for the current settings.""" + styles = [] if not self._cssStyles: - return theStyles + return styles mScale = self._lineHeight/1.15 textAlign = "justify" if self._doJustify else "left" - theStyles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format( + styles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format( self._textFont, self._textSize )) - theStyles.append(( + styles.append(( "p {{" "text-align: {0}; line-height: {1:d}%; " "margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;" @@ -359,7 +359,7 @@ class ToHtml(Tokenizer): mScale * self._marginText[0], mScale * self._marginText[1], )) - theStyles.append(( + styles.append(( "h1 {{" "color: rgb(66, 113, 174); " "page-break-after: avoid; " @@ -369,7 +369,7 @@ class ToHtml(Tokenizer): ).format( mScale * self._marginHead1[0], mScale * self._marginHead1[1] )) - theStyles.append(( + styles.append(( "h2 {{" "color: rgb(66, 113, 174); " "page-break-after: avoid; " @@ -379,7 +379,7 @@ class ToHtml(Tokenizer): ).format( mScale * self._marginHead2[0], mScale * self._marginHead2[1] )) - theStyles.append(( + styles.append(( "h3 {{" "color: rgb(50, 50, 50); " "page-break-after: avoid; " @@ -389,7 +389,7 @@ class ToHtml(Tokenizer): ).format( mScale * self._marginHead3[0], mScale * self._marginHead3[1] )) - theStyles.append(( + styles.append(( "h4 {{" "color: rgb(50, 50, 50); " "page-break-after: avoid; " @@ -399,7 +399,7 @@ class ToHtml(Tokenizer): ).format( mScale * self._marginHead4[0], mScale * self._marginHead4[1] )) - theStyles.append(( + styles.append(( ".title {{" "font-size: 2.5em; " "margin-top: {0:.2f}em; " @@ -408,7 +408,7 @@ class ToHtml(Tokenizer): ).format( mScale * self._marginTitle[0], mScale * self._marginTitle[1] )) - theStyles.append(( + styles.append(( ".sep, .skip {{" "text-align: center; " "margin-top: {0:.2f}em; " @@ -418,61 +418,58 @@ class ToHtml(Tokenizer): mScale, mScale )) - theStyles.append("a {color: rgb(66, 113, 174);}") - theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}") - theStyles.append(".break {text-align: left;}") - theStyles.append(".synopsis {font-style: italic;}") - theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}") + styles.append("a {color: rgb(66, 113, 174);}") + styles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}") + styles.append(".break {text-align: left;}") + styles.append(".synopsis {font-style: italic;}") + styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}") - return theStyles + return styles ## # Internal Functions ## - def _formatSynopsis(self, tText): - """Apply HTML formatting to synopsis. - """ + def _formatSynopsis(self, text: str) -> str: + """Apply HTML formatting to synopsis.""" if self._genMode == self.M_PREVIEW: sSynop = self._trSynopsis - return f"

{sSynop}: {tText}

\n" + return f"

{sSynop}: {text}

\n" else: sSynop = self._localLookup("Synopsis") - return f"

{sSynop}: {tText}

\n" + return f"

{sSynop}: {text}

\n" - def _formatComments(self, tText): - """Apply HTML formatting to comments. - """ + def _formatComments(self, text: str) -> str: + """Apply HTML formatting to comments.""" if self._genMode == self.M_PREVIEW: - return f"

{tText}

\n" + return f"

{text}

\n" else: sComm = self._localLookup("Comment") - return f"

{sComm}: {tText}

\n" + return f"

{sComm}: {text}

\n" - def _formatKeywords(self, tText): - """Apply HTML formatting to keywords. - """ - isValid, theBits, _ = self._project.index.scanThis("@"+tText) - if not isValid or not theBits: + def _formatKeywords(self, text: str) -> str: + """Apply HTML formatting to keywords.""" + valid, bits, _ = self._project.index.scanThis("@"+text) + if not valid or not bits: return "" - retText = "" - refTags = [] - if theBits[0] in nwLabels.KEY_NAME: - retText += f"{nwLabels.KEY_NAME[theBits[0]]}: " - if len(theBits) > 1: - if theBits[0] == nwKeyWords.TAG_KEY: - retText += f"{theBits[1]}" + 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]}" else: if self._genMode == self.M_PREVIEW: - for tTag in theBits[1:]: - refTags.append(f"{tTag}") - retText += ", ".join(refTags) + for tTag in bits[1:]: + tags.append(f"{tTag}") + result += ", ".join(tags) else: - for tTag in theBits[1:]: - refTags.append(f"{tTag}") - retText += ", ".join(refTags) + for tTag in bits[1:]: + tags.append(f"{tTag}") + result += ", ".join(tags) - return retText + return result # END Class ToHtml diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 010695f0..34307a93 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -29,6 +29,7 @@ import re import logging from abc import ABC, abstractmethod +from pathlib import Path from operator import itemgetter from functools import partial @@ -102,7 +103,7 @@ class Tokenizer(ABC): self._result = "" # The result of the last document self._keepMarkdown = False # Whether to keep the markdown text - self._theMarkdown = [] # The result novelWriter markdown of all documents + self._allMarkdown = [] # The result novelWriter markdown of all documents # User Settings self._textFont = "Serif" # Output text font @@ -164,111 +165,136 @@ class Tokenizer(ABC): ## @property - def theResult(self): + def theResult(self) -> str: + """The result of the build process.""" return self._result @property - def theMarkdown(self): - return self._theMarkdown + def theMarkdown(self) -> list: + """The combined novelWriter Markdown text.""" + return self._allMarkdown @property - def errData(self): + def errData(self) -> list: + """The error data.""" return self._errData ## # Setters ## - def setTitleFormat(self, hFormat): + def setTitleFormat(self, hFormat: str): + """Set the title format pattern.""" self._fmtTitle = hFormat.strip() return - def setChapterFormat(self, hFormat): + def setChapterFormat(self, hFormat: str): + """Set the chapert format pattern.""" self._fmtChapter = hFormat.strip() return - def setUnNumberedFormat(self, hFormat): + def setUnNumberedFormat(self, hFormat: str): + """Set the unnumbered format pattern.""" self._fmtUnNum = hFormat.strip() return - def setSceneFormat(self, hFormat, hide): + def setSceneFormat(self, hFormat: str, hide: bool): + """Set the scene format pattern and hidden status.""" self._fmtScene = hFormat.strip() self._hideScene = hide return - def setSectionFormat(self, hFormat, hide): + def setSectionFormat(self, hFormat: str, hide: bool): + """Set the section format pattern and hidden status.""" self._fmtSection = hFormat.strip() self._hideSection = hide return - def setFont(self, textFont, textSize, textFixed=False): - self._textFont = textFont - self._textSize = round(int(textSize)) - self._textFixed = textFixed + def setFont(self, family: str, size: int, isFixed: bool = False): + """Set the build font.""" + self._textFont = family + self._textSize = round(int(size)) + self._textFixed = isFixed return - def setLineHeight(self, lineHeight): - self._lineHeight = min(max(float(lineHeight), 0.5), 5.0) + def setLineHeight(self, height: float): + """Set the line height between 0.5 and 5.0.""" + self._lineHeight = min(max(float(height), 0.5), 5.0) return - def setBlockIndent(self, blockIndent): - self._blockIndent = min(max(float(blockIndent), 0.0), 10.0) + def setBlockIndent(self, indent: float): + """Set the block indent between 0.0 and 10.0.""" + self._blockIndent = min(max(float(indent), 0.0), 10.0) return - def setJustify(self, doJustify): - self._doJustify = doJustify + def setJustify(self, state: bool): + """Enable or disable text justification.""" + self._doJustify = state return - def setTitleMargins(self, mUpper, mLower): - self._marginTitle = (float(mUpper), float(mLower)) + def setTitleMargins(self, upper: float, lower: float): + """Set the upper and lower title margin.""" + self._marginTitle = (float(upper), float(lower)) return - def setHead1Margins(self, mUpper, mLower): - self._marginHead1 = (float(mUpper), float(mLower)) + def setHead1Margins(self, upper: float, lower: float): + """Set the upper and lower header 1 margin.""" + self._marginHead1 = (float(upper), float(lower)) return - def setHead2Margins(self, mUpper, mLower): - self._marginHead2 = (float(mUpper), float(mLower)) + def setHead2Margins(self, upper: float, lower: float): + """Set the upper and lower header 2 margin.""" + self._marginHead2 = (float(upper), float(lower)) return - def setHead3Margins(self, mUpper, mLower): - self._marginHead3 = (float(mUpper), float(mLower)) + def setHead3Margins(self, upper: float, lower: float): + """Set the upper and lower header 3 margin.""" + self._marginHead3 = (float(upper), float(lower)) return - def setHead4Margins(self, mUpper, mLower): - self._marginHead4 = (float(mUpper), float(mLower)) + def setHead4Margins(self, upper: float, lower: float): + """Set the upper and lower header 4 margin.""" + self._marginHead4 = (float(upper), float(lower)) return - def setTextMargins(self, mUpper, mLower): - self._marginText = (float(mUpper), float(mLower)) + def setTextMargins(self, upper: float, lower: float): + """Set the upper and lower text margin.""" + self._marginText = (float(upper), float(lower)) return - def setMetaMargins(self, mUpper, mLower): - self._marginMeta = (float(mUpper), float(mLower)) + def setMetaMargins(self, upper: float, lower: float): + """Set the upper and lower meta text margin.""" + self._marginMeta = (float(upper), float(lower)) return - def setLinkHeaders(self, linkHeaders): - self._linkHeaders = linkHeaders + def setLinkHeaders(self, state: bool): + """Enable or disable adding an anchor before headers.""" + self._linkHeaders = state return - def setBodyText(self, doBodyText): - self._doBodyText = doBodyText + def setBodyText(self, state: bool): + """Include body text in build.""" + self._doBodyText = state return - def setSynopsis(self, doSynopsis): - self._doSynopsis = doSynopsis + def setSynopsis(self, state: bool): + """Include synopsis comments in build.""" + self._doSynopsis = state return - def setComments(self, doComments): - self._doComments = doComments + def setComments(self, state: bool): + """Include comments in build.""" + self._doComments = state return - def setKeywords(self, doKeywords): - self._doKeywords = doKeywords + def setKeywords(self, state: bool): + """Include keywords in build.""" + self._doKeywords = state return - def setKeepMarkdown(self, keepMarkdown): - self._keepMarkdown = keepMarkdown + def setKeepMarkdown(self, state: bool): + """Keep original markdown during build.""" + self._keepMarkdown = state return ## @@ -279,10 +305,9 @@ class Tokenizer(ABC): def doConvert(self): raise NotImplementedError - def addRootHeading(self, theHandle): - """Add a heading at the start of a new root folder. - """ - if not self._project.tree.checkType(theHandle, nwItemType.ROOT): + def addRootHeading(self, tHandle: str) -> bool: + """Add a heading at the start of a new root folder.""" + if not self._project.tree.checkType(tHandle, nwItemType.ROOT): return False if self._isFirst: @@ -291,7 +316,7 @@ class Tokenizer(ABC): else: textAlign = self.A_PBB | self.A_CENTRE - theItem = self._project.tree[theHandle] + theItem = self._project.tree[tHandle] locNotes = self._localLookup("Notes") theTitle = f"{locNotes}: {theItem.itemName}" self._tokens = [] @@ -299,22 +324,22 @@ class Tokenizer(ABC): self.T_TITLE, 0, theTitle, None, textAlign )) if self._keepMarkdown: - self._theMarkdown.append(f"# {theTitle}\n\n") + self._allMarkdown.append(f"# {theTitle}\n\n") return True - def setText(self, theHandle, theText=None): + def setText(self, tHandle: str, text: str | None = None) -> bool: """Set the text for the tokenizer from a handle. If theText is not set, load it from the file. """ - self._nwItem = self._project.tree[theHandle] + self._nwItem = self._project.tree[tHandle] if self._nwItem is None: return False - if theText is None: - theText = self._project.storage.getDocument(theHandle).readDocument() or "" + if text is None: + text = self._project.storage.getDocument(tHandle).readDocument() or "" - self._text = theText + self._text = text docSize = len(self._text) if docSize > nwConst.MAX_DOCSIZE: @@ -331,8 +356,7 @@ class Tokenizer(ABC): return True def doPreProcessing(self): - """Run trough the various replace doctionaries. - """ + """Run trough the various replace dictionaries.""" # Process the user's auto-replace dictionary autoReplace = self._project.data.autoReplace if len(autoReplace) > 0: @@ -582,7 +606,7 @@ class Tokenizer(ABC): tmpMarkdown.append("\n") if self._keepMarkdown: - self._theMarkdown.append("".join(tmpMarkdown)) + self._allMarkdown.append("".join(tmpMarkdown)) # Second Pass # =========== @@ -610,7 +634,7 @@ class Tokenizer(ABC): return - def doHeaders(self): + def doHeaders(self) -> bool: """Apply formatting to the text headers for novel files. This also applies chapter and scene numbering. """ @@ -708,11 +732,10 @@ class Tokenizer(ABC): return True - def saveRawMarkdown(self, savePath): - """Save the data to a plain text file. - """ + def saveRawMarkdown(self, savePath: str | Path): + """Save the data to a plain text file.""" with open(savePath, mode="w", encoding="utf-8") as outFile: - for nwdPage in self._theMarkdown: + for nwdPage in self._allMarkdown: outFile.write(nwdPage) return diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py index b59aed6f..67eaf9a4 100644 --- a/novelwriter/core/tomd.py +++ b/novelwriter/core/tomd.py @@ -22,10 +22,14 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import logging +from pathlib import Path + from novelwriter.constants import nwLabels +from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer logger = logging.getLogger(__name__) @@ -36,8 +40,8 @@ class ToMarkdown(Tokenizer): M_STD = 0 # Standard Markdown M_GH = 1 # GitHub Markdown - def __init__(self, theProject): - super().__init__(theProject) + def __init__(self, project: NWProject): + super().__init__(project) self._genMode = self.M_STD self._fullMD = [] @@ -49,7 +53,8 @@ class ToMarkdown(Tokenizer): ## @property - def fullMD(self): + def fullMD(self) -> list: + """Return the markdown as a list.""" return self._fullMD ## @@ -68,9 +73,8 @@ class ToMarkdown(Tokenizer): # Class Methods ## - def getFullResultSize(self): - """Return the size of the full Markdown result. - """ + def getFullResultSize(self) -> int: + """Return the size of the full Markdown result.""" return sum([len(x) for x in self._fullMD]) def doConvert(self): @@ -166,49 +170,36 @@ class ToMarkdown(Tokenizer): return - def saveMarkdown(self, savePath): - """Save the data to a plain text file. - """ - with open(savePath, mode="w", encoding="utf-8") as outFile: - theText = "".join(self._fullMD) - outFile.write(theText) - + def saveMarkdown(self, path: str | Path): + """Save the data to a plain text file.""" + with open(path, mode="w", encoding="utf-8") as outFile: + outFile.write("".join(self._fullMD)) return - def replaceTabs(self, nSpaces=8, spaceChar=" "): - """Replace tabs with spaces. - """ - fullMD = [] - eightSpace = spaceChar*nSpaces - for aPage in self._fullMD: - fullMD.append(aPage.replace("\t", eightSpace)) - - self._fullMD = fullMD + def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " "): + """Replace tabs with spaces.""" + spaces = spaceChar*nSpaces + self._fullMD = [p.replace("\t", spaces) for p in self._fullMD] return ## # Internal Functions ## - def _formatKeywords(self, tText, tStyle): - """Apply Markdown formatting to keywords. - """ - isValid, theBits, _ = self._project.index.scanThis("@"+tText) - if not isValid or not theBits: + def _formatKeywords(self, text: str, style: int) -> str: + """Apply Markdown formatting to keywords.""" + valid, bits, _ = self._project.index.scanThis("@"+text) + if not valid or not bits: return "" - retText = "" - if theBits[0] in nwLabels.KEY_NAME: - retText += f"**{nwLabels.KEY_NAME[theBits[0]]}:** " + result = "" + if bits[0] in nwLabels.KEY_NAME: + result += f"**{nwLabels.KEY_NAME[bits[0]]}:** " + if len(bits) > 1: + result += ", ".join(bits[1:]) - if len(theBits) > 1: - retText += ", ".join(theBits[1:]) + result += " \n" if style & self.A_Z_BTMMRG else "\n\n" - if tStyle & self.A_Z_BTMMRG: - retText += " \n" - else: - retText += "\n\n" - - return retText + return result # END Class ToMarkdown