Add annotations to tokenizer, html and markdown classes

This commit is contained in:
Veronica Berglyd Olsen
2023-06-06 00:35:57 +02:00
parent 850ee73ed4
commit 6b6c517b1f
3 changed files with 177 additions and 166 deletions
+60 -63
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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="&nbsp;"):
"""Replace tabs with spaces in the html.
"""
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = "&nbsp;"):
"""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"<p class='comment'><span class='synopsis'>{sSynop}:</span> {tText}</p>\n"
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
else:
sSynop = self._localLookup("Synopsis")
return f"<p class='synopsis'><strong>{sSynop}:</strong> {tText}</p>\n"
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\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"<p class='comment'>{tText}</p>\n"
return f"<p class='comment'>{text}</p>\n"
else:
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {tText}</p>\n"
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\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"<span class='tags'>{nwLabels.KEY_NAME[theBits[0]]}:</span> "
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += f"<a name='tag_{theBits[1]}'>{theBits[1]}</a>"
result = ""
tags = []
if bits[0] in nwLabels.KEY_NAME:
result += f"<span class='tags'>{nwLabels.KEY_NAME[bits[0]]}:</span> "
if len(bits) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
else:
if self._genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append(f"<a href='#{theBits[0][1:]}={tTag}'>{tTag}</a>")
retText += ", ".join(refTags)
for tTag in bits[1:]:
tags.append(f"<a href='#{bits[0][1:]}={tTag}'>{tTag}</a>")
result += ", ".join(tags)
else:
for tTag in theBits[1:]:
refTags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
retText += ", ".join(refTags)
for tTag in bits[1:]:
tags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
result += ", ".join(tags)
return retText
return result
# END Class ToHtml
+88 -65
View File
@@ -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
+29 -38
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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