Refactor core and base classes (#931)

* Improve the NWIndex class
* Improve the NWItem and NWDoc classes
* Make some optimisations in projects and update tests
* Minor changes to string formatting in main source file
* Add some more protection to core classes
* Make all converter class attributes private
This commit is contained in:
Veronica Berglyd Olsen
2021-12-18 16:03:45 +01:00
committed by GitHub
parent 2f75d88694
commit 5e2ab4f612
20 changed files with 889 additions and 866 deletions
+23 -20
View File
@@ -56,15 +56,21 @@ class NWDoc():
return
def __repr__(self):
return f"<NWDoc handle={self._docHandle}>"
def __bool__(self):
return self._docHandle is not None and bool(self._theItem)
##
# Class Methods
##
def readDocument(self, isOrphan=False):
"""Read a document from set handle, capturing potential file
system errors and parse meta data. If the document doesn't exist
on disk, return an empty string. If something went wrong, return
None.
"""Read the document specified by the handle set in the
contructor, capturing potential file system errors and parse
meta data. If the document doesn't exist on disk, return an
empty string. If something went wrong, return None.
"""
self._docError = ""
if self._docHandle is None:
@@ -88,7 +94,6 @@ class NWDoc():
if os.path.isfile(docPath):
try:
with open(docPath, mode="r", encoding="utf-8") as inFile:
# Check the first <= 10 lines for metadata
for i in range(10):
inLine = inFile.readline()
@@ -108,14 +113,15 @@ class NWDoc():
else:
# The document file does not exist, so we assume it's a new
# document and initialise an empty text string.
logger.debug("The requested document does not exist.")
logger.debug("The requested document does not exist")
return ""
return theText
def writeDocument(self, docText, forceWrite=False):
"""Write the document. The file is saved via a temp file in case
of save failure. Returns True if successful, False if not.
"""Write the document specified by the handle attribute. Handle
any IO errors in the process Returns True if successful, False
if not.
"""
self._docError = ""
if self._docHandle is None:
@@ -156,9 +162,7 @@ class NWDoc():
# If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file
if os.path.isfile(docPath):
os.unlink(docPath)
os.rename(docTemp, docPath)
os.replace(docTemp, docPath)
self._prevHash = sha256sum(docPath)
self._currHash = self._prevHash
@@ -174,11 +178,10 @@ class NWDoc():
logger.error("No document handle set")
return False
docFile = self._docHandle+".nwd"
chkList = []
chkList.append(os.path.join(self.theProject.projContent, docFile))
chkList.append(os.path.join(self.theProject.projContent, docFile+"~"))
chkList = [
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"),
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"),
]
for chkFile in chkList:
if os.path.isfile(chkFile):
@@ -196,18 +199,18 @@ class NWDoc():
##
def getFileLocation(self):
"""Return the file location of the current file.
"""Return the file location of the current document.
"""
return self._fileLoc
def getCurrentItem(self):
"""Return a pointer to the currently open item.
"""Return a pointer to the currently open NWItem.
"""
return self._theItem
def getMeta(self):
"""Parses the document meta tag and returns the path and name as
a list and a string.
"""Parse the document meta tag and return the name, parent,
class and layout meta values.
"""
theName = self._docMeta.get("name", "")
theParent = self._docMeta.get("parent", None)
+45 -74
View File
@@ -27,7 +27,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import json
import logging
import novelwriter
from time import time
@@ -49,10 +48,10 @@ class NWIndex():
def __init__(self, theProject):
self.theProject = theProject
# Internal
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
self.indexBroken = False
self._indexBroken = False
# Indices
self._tagIndex = {}
@@ -67,6 +66,10 @@ class NWIndex():
return
@property
def indexBroken(self):
return self._indexBroken
##
# Public Methods
##
@@ -88,11 +91,7 @@ class NWIndex():
"""
logger.debug("Removing item '%s' from the index", tHandle)
delTags = []
for tTag in self._tagIndex:
if self._tagIndex[tTag][1] == tHandle:
delTags.append(tTag)
delTags = list(filter(lambda x: self._tagIndex[x][1] == tHandle, self._tagIndex))
for tTag in delTags:
self._tagIndex.pop(tTag, None)
@@ -157,7 +156,7 @@ class NWIndex():
except Exception:
logger.error("Failed to load index file")
logException()
self.indexBroken = True
self._indexBroken = True
return False
self._tagIndex = theData.get("tagIndex", {})
@@ -214,17 +213,17 @@ class NWIndex():
self._checkRefIndex()
self._checkFileIndex()
self._checkFileMeta()
self.indexBroken = False
self._indexBroken = False
except Exception:
logger.error("Error while checking index")
logException()
self.indexBroken = True
self._indexBroken = True
logger.verbose("Index check took %.3f ms", (time() - tStart)*1000)
logger.debug("Index check complete")
if self.indexBroken:
if self._indexBroken:
self.clearIndex()
return
@@ -237,7 +236,8 @@ class NWIndex():
"""Scan a piece of text associated with a handle. This will
update the indices accordingly. This function takes the handle
and text as separate inputs as we want to primarily scan the
files before we save them, unless we're rebuilding the index.
files before we save them in which case we already have the
text.
"""
theItem = self.theProject.projTree[tHandle]
theRoot = self.theProject.projTree.getRootItem(tHandle)
@@ -259,7 +259,7 @@ class NWIndex():
cC, wC, pC = countWords(theText)
self._fileMeta[tHandle] = ["H0", cC, wC, pC]
# If the file is archived or trashed, we don't index the file itself
# If the file is archived or in trash, we don't index the content
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
logger.debug("Not indexing trash item '%s'", tHandle)
return False
@@ -276,16 +276,12 @@ class NWIndex():
self._refIndex.pop(tHandle, None)
self._fileIndex[tHandle] = {}
# Also clear references to file in tag index
clearTags = []
for aTag in self._tagIndex:
if self._tagIndex[aTag][1] == tHandle:
clearTags.append(aTag)
# Also clear references to the file in the tags index
clearTags = list(filter(lambda x: self._tagIndex[x][1] == tHandle, self._tagIndex))
for aTag in clearTags:
self._tagIndex.pop(aTag)
# Scan the text content
nLine = 0
nTitle = 0
theLines = theText.splitlines()
for nLine, aLine in enumerate(theLines, start=1):
@@ -355,7 +351,7 @@ class NWIndex():
hText = aLine[5:].strip()
elif aLine.startswith("#! "):
hDepth = "H1"
hText = aLine[2:].strip()
hText = aLine[3:].strip()
elif aLine.startswith("##! "):
hDepth = "H2"
hText = aLine[4:].strip()
@@ -374,6 +370,8 @@ class NWIndex():
}
if self._fileMeta[tHandle][0] == "H0":
# Since this initialises to H0, this ensures that only the
# first header level is recorded in the file meta index
self._fileMeta[tHandle][0] = hDepth
return True
@@ -542,8 +540,7 @@ class NWIndex():
hCount = [0, 0, 0, 0, 0]
for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in self._fileIndex[tHandle]:
theData = self._fileIndex[tHandle][sTitle]
iLevel = H_LEVEL.get(theData["level"], 0)
iLevel = H_LEVEL.get(self._fileIndex[tHandle][sTitle]["level"], 0)
hCount[iLevel] += 1
return hCount
@@ -551,45 +548,29 @@ class NWIndex():
def getHandleWordCounts(self, tHandle):
"""Get all header word counts for a specific handle.
"""
theCounts = []
hRecord = self._fileIndex.get(tHandle, None)
if hRecord is None:
return theCounts
for sTitle, sData in hRecord.items():
theCounts.append((f"{tHandle}:{sTitle}", sData["wCount"]))
return theCounts
hRecord = self._fileIndex.get(tHandle, {})
return [(f"{tHandle}:{sTitle}", sData["wCount"]) for sTitle, sData in hRecord.items()]
def getHandleHeaders(self, tHandle):
"""Get all headers for a specific handle.
"""
theHeaders = []
hRecord = self._fileIndex.get(tHandle, None)
if hRecord is None:
return theHeaders
for sTitle, sData in hRecord.items():
theHeaders.append((sTitle, sData["level"], sData["title"]))
return theHeaders
hRecord = self._fileIndex.get(tHandle, {})
return [(sTitle, sData["level"], sData["title"]) for sTitle, sData in hRecord.items()]
def getHandleHeaderLevel(self, tHandle):
"""Get the header level of the first header of a handle.
"""
if tHandle in self._fileMeta:
return self._fileMeta[tHandle][0]
return "H0"
return self._fileMeta.get(tHandle, ["H0"])[0]
def getTableOfContents(self, maxDepth, skipExcluded=True):
"""Generate a table of contents up to a maxiumum depth.
"""Generate a table of contents up to a maximum depth.
"""
tOrder = []
tData = {}
pKey = None
for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in sorted(self._fileIndex[tHandle]):
tKey = "%s:%s" % (tHandle, sTitle)
tKey = f"{tHandle}:{sTitle}"
theData = self._fileIndex[tHandle][sTitle]
iLevel = H_LEVEL.get(theData["level"], 0)
if iLevel > maxDepth:
@@ -605,19 +586,17 @@ class NWIndex():
"words": theData["wCount"],
}
theToC = []
for tKey in tOrder:
theToC.append((
tKey,
tData[tKey]["level"],
tData[tKey]["title"],
tData[tKey]["words"],
))
theToC = [(
tKey,
tData[tKey]["level"],
tData[tKey]["title"],
tData[tKey]["words"]
) for tKey in tOrder]
return theToC
def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file
"""Return the counts for a file, or a section of a file,
starting at title sTitle if it is provided.
"""
cC = 0
@@ -640,12 +619,9 @@ class NWIndex():
def getReferences(self, tHandle, sTitle=None):
"""Extract all references made in a file, and optionally title
section. sTitle must be a string.
section.
"""
theRefs = {}
for tKey in nwKeyWords.KEY_CLASS:
theRefs[tKey] = []
theRefs = {x: [] for x in nwKeyWords.KEY_CLASS}
if tHandle not in self._refIndex:
return theRefs
@@ -669,15 +645,11 @@ class NWIndex():
"""Build a list of files referring back to our file, specified
by tHandle.
"""
theRefs = {}
if tHandle is None:
return theRefs
theTags = set()
for tTag in self._tagIndex:
if tHandle == self._tagIndex[tTag][1]:
theTags.add(tTag)
return {}
theRefs = {}
theTags = set(filter(lambda x: self._tagIndex[x][1] == tHandle, self._tagIndex))
if theTags:
for tHandle in self._refIndex:
for sTitle in self._refIndex[tHandle]:
@@ -690,10 +662,9 @@ class NWIndex():
def getTagSource(self, theTag):
"""Return the source location of a given tag.
"""
if theTag in self._tagIndex:
theRef = self._tagIndex[theTag]
if len(theRef) == 4:
return theRef[1], theRef[0], theRef[3]
theRef = self._tagIndex.get(theTag, [])
if len(theRef) == 4:
return theRef[1], theRef[0], theRef[3]
return None, 0, "T000000"
##
@@ -859,9 +830,9 @@ def countWords(theText):
return charCount, wordCount, paraCount
# We need to treat dashes as word separators for counting words.
# The check+replace apprach is much faster that direct replace for
# The check+replace approach is much faster than direct replace for
# large texts, and a bit slower for small texts, but in the latter
# case it doesn't matter.
# case it doesn't really matter.
if nwUnicode.U_ENDASH in theText:
theText = theText.replace(nwUnicode.U_ENDASH, " ")
if nwUnicode.U_EMDASH in theText:
+17 -16
View File
@@ -67,7 +67,7 @@ class NWItem():
##
def packXML(self, xParent):
"""Packs all the data in the class instance into an XML object.
"""Pack all the data in the class instance into an XML object.
"""
xPack = etree.SubElement(xParent, "item", attrib={
"handle": str(self.itemHandle),
@@ -91,7 +91,7 @@ class NWItem():
return
def unpackXML(self, xItem):
"""Sets the values from an XML entry of type 'item'.
"""Set the values from an XML entry of type 'item'.
"""
if xItem.tag != "item":
logger.error("XML entry is not an NWItem")
@@ -133,7 +133,7 @@ class NWItem():
else:
# Sliently skip as we may otherwise cause orphaned
# items if an otherwise valid file is opened by a
# version of novelWriter that doesn't know the tag.
# version of novelWriter that doesn't know the tag
logger.error("Unknown tag '%s'", xValue.tag)
# Guarantees that <status> is parsed after <class>
@@ -143,7 +143,7 @@ class NWItem():
@staticmethod
def _subPack(xParent, name, attrib=None, text=None, none=True):
"""Packs the values into an xml element.
"""Pack the values into an XML element.
"""
if not none and (text is None or text == "None"):
return None
@@ -204,7 +204,7 @@ class NWItem():
return
def setParent(self, theParent):
"""Set the parent handle, and ensure that it is valid.
"""Set the parent handle, and ensure it is valid.
"""
if theParent is None:
self.itemParent = None
@@ -216,14 +216,15 @@ class NWItem():
def setOrder(self, theOrder):
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, not actually used by novelWriter.
is purely a meta value, and not actually used by novelWriter at
the moment.
"""
self.itemOrder = checkInt(theOrder, 0)
return
def setType(self, theType):
"""Set the item type from either a proper nwItemType, or set it
from a string representing a nwItemType.
from a string representing an nwItemType.
"""
if isinstance(theType, nwItemType):
self.itemType = theType
@@ -236,7 +237,7 @@ class NWItem():
def setClass(self, theClass):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing a nwItemClass.
it from a string representing an nwItemClass.
"""
if isinstance(theClass, nwItemClass):
self.itemClass = theClass
@@ -249,7 +250,7 @@ class NWItem():
def setLayout(self, theLayout):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing a nwItemLayout.
it from a string representing an nwItemLayout.
"""
if isinstance(theLayout, nwItemLayout):
self.itemLayout = theLayout
@@ -273,7 +274,7 @@ class NWItem():
return
def setExpanded(self, expState):
"""Save the expanded status of an item in the project tree.
"""Set the expanded status of an item in the project tree.
"""
if isinstance(expState, str):
self.isExpanded = (expState == str(True))
@@ -282,7 +283,7 @@ class NWItem():
return
def setExported(self, expState):
"""Save the export flag.
"""Set the export flag.
"""
if isinstance(expState, str):
self.isExported = (expState == str(True))
@@ -297,29 +298,29 @@ class NWItem():
def setCharCount(self, theCount):
"""Set the character count, and ensure that it is an integer.
"""
self.charCount = checkInt(theCount, 0)
self.charCount = max(0, checkInt(theCount, 0))
return
def setWordCount(self, theCount):
"""Set the word count, and ensure that it is an integer.
"""
self.wordCount = checkInt(theCount, 0)
self.wordCount = max(0, checkInt(theCount, 0))
return
def setParaCount(self, theCount):
"""Set the paragraph count, and ensure that it is an integer.
"""
self.paraCount = checkInt(theCount, 0)
self.paraCount = max(0, checkInt(theCount, 0))
return
def setCursorPos(self, thePosition):
"""Set the cursor position, and ensure that it is an integer.
"""
self.cursorPos = checkInt(thePosition, 0)
self.cursorPos = max(0, checkInt(thePosition, 0))
return
def saveInitialCount(self):
"""Set the initial word count.
"""Save the initial word count.
"""
self.initCount = self.wordCount
return
+1 -1
View File
@@ -123,7 +123,7 @@ class OptionState():
##
def setValue(self, group, name, value):
"""Saves a value, with a given group and name.
"""Save a value, with a given group and name.
"""
if group not in VALID_MAP:
logger.error("Unknown option group '%s'", group)
+25 -22
View File
@@ -237,12 +237,13 @@ class NWProject():
return
def newProject(self, projData=None):
def newProject(self, projData):
"""Create a new project by populating the project tree with a
few starter items.
"""
if projData is None:
projData = {}
if not isinstance(projData, dict):
logger.error("Invalid call to newProject function")
return False
popMinimal = projData.get("popMinimal", True)
popCustom = projData.get("popCustom", False)
@@ -473,8 +474,8 @@ class NWProject():
# 1.2 : Changes the way autoReplace entries are stored. The 1.1
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
# 1.3 : Reduces the number of layouts to onlye two. One for
# novel documents and one for project notes. Introduced in
# 1.3 : Reduces the number of layouts to only two. One for novel
# documents and one for project notes. Introduced in
# version 1.5.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
@@ -630,8 +631,8 @@ class NWProject():
def saveProject(self, autoSave=False):
"""Save the project main XML file. The saving command itself
uses a temporary filename, and the file is renamed afterwards to
make sure if the save fails, we're not left with a truncated
uses a temporary filename, and the file is replaced afterwards
to make sure if the save fails, we're not left with a truncated
file.
"""
if self.projPath is None:
@@ -720,11 +721,15 @@ class NWProject():
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
if os.path.isfile(backFile):
os.unlink(backFile)
if os.path.isfile(saveFile):
os.rename(saveFile, backFile)
os.rename(tempFile, saveFile)
try:
if os.path.isfile(saveFile):
os.replace(saveFile, backFile)
os.replace(tempFile, saveFile)
except Exception as exc:
self.theParent.makeAlert(self.tr(
"Failed to save project."
), nwAlert.ERROR, exception=exc)
return False
# Save project GUI options
self.optState.saveSettings()
@@ -857,9 +862,9 @@ class NWProject():
def extractSampleProject(self, projData):
"""Make a copy of the sample project.
First, try to copy the content of the sample folder to the new
project path, or if the folder doesn't exist, look for the zip
file in the assets folder.
First, look for the sample.zip file in the assets folder and
unpack it. If it doesn't exist, try to copy the content of the
sample folder to the new project path. If neither exits, error.
"""
projPath = projData.get("projPath", None)
if projPath is None:
@@ -965,14 +970,14 @@ class NWProject():
return True
def setBookTitle(self, bookTitle):
"""Set the boom title, that is, the title to include in exports.
"""Set the book title, that is, the title to include in exports.
"""
self.bookTitle = bookTitle.strip()
self.setProjectChanged(True)
return True
def setBookAuthors(self, bookAuthors):
"""A line separated list of book authors, parsed into an array.
"""A line-separated list of authors, parsed into an array.
"""
if not isinstance(bookAuthors, str):
return False
@@ -1097,8 +1102,7 @@ class NWProject():
return True
def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary. This replaces the entire
dictionary, so alterations have to be made in a copy.
"""Update the auto-replace dictionary.
"""
self.autoReplace = autoReplace
self.setProjectChanged(True)
@@ -1128,7 +1132,7 @@ class NWProject():
##
def getAuthors(self):
"""Returns a formatted string of authors.
"""Return a formatted string of authors.
"""
nAuth = len(self.bookAuthors)
authString = ""
@@ -1225,8 +1229,7 @@ class NWProject():
return it. The variable is cast to a string before lookup. If
the word does not exist, it returns itself.
"""
theValue = str(theWord)
return self.langData.get(theValue, theValue)
return self.langData.get(str(theWord), str(theWord))
##
# Internal Functions
-3
View File
@@ -25,7 +25,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import logging
import novelwriter
from novelwriter.error import logException
@@ -36,8 +35,6 @@ class NWSpellEnchant():
def __init__(self):
self.mainConf = novelwriter.CONFIG
self._theDict = None
self._projDict = set()
self._projectDict = None
+49 -41
View File
@@ -40,9 +40,9 @@ class ToHtml(Tokenizer):
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
self.genMode = self.M_EXPORT
self.cssStyles = True
self.fullHTML = []
self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML = []
# Internals
self._trMap = {}
@@ -50,6 +50,14 @@ class ToHtml(Tokenizer):
return
##
# Properties
##
@property
def fullHTML(self):
return self._fullHTML
##
# Setters
##
@@ -59,17 +67,17 @@ class ToHtml(Tokenizer):
need to make a few changes to formatting, which is managed by
these flags.
"""
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.doSynopsis = doSynopsis
self._genMode = self.M_PREVIEW
self._doKeywords = True
self._doComments = doComments
self._doSynopsis = doSynopsis
return
def setStyles(self, cssStyles):
"""Enable/disable CSS styling. Some elements may still have
class tags.
"""
self.cssStyles = cssStyles
self._cssStyles = cssStyles
return
def setReplaceUnicode(self, doReplace):
@@ -93,21 +101,21 @@ class ToHtml(Tokenizer):
def getFullResultSize(self):
"""Return the size of the full HTML result.
"""
return sum([len(x) for x in self.fullHTML])
return sum([len(x) for x in self._fullHTML])
def doPreProcessing(self):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
Tokenizer.doPreProcessing(self)
self.theText = self.theText.translate(self._trMap)
self._theText = self._theText.translate(self._trMap)
return
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
"""
if self.genMode == self.M_PREVIEW:
if self._genMode == self.M_PREVIEW:
htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "<b>",
self.FMT_B_E: "</b>",
@@ -126,7 +134,7 @@ class ToHtml(Tokenizer):
self.FMT_D_E: "</del>",
}
if self.isNovel and self.genMode != self.M_PREVIEW:
if self._isNovel and self._genMode != self.M_PREVIEW:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
@@ -140,13 +148,13 @@ class ToHtml(Tokenizer):
h3 = "h3"
h4 = "h4"
self.theResult = ""
self._theResult = ""
thisPar = []
parStyle = None
tmpResult = []
for tType, tLine, tDirty, tFormat, tStyle in self.theTokens:
for tType, tLine, tDirty, tFormat, tStyle in self._theTokens:
# Replace < and > and recompute formatting positions
cText = []
@@ -168,7 +176,7 @@ class ToHtml(Tokenizer):
# Styles
aStyle = []
if tStyle is not None and self.cssStyles:
if tStyle is not None and self._cssStyles:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
elif tStyle & self.A_RIGHT:
@@ -200,7 +208,7 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
if self.linkHeaders:
if self._linkHeaders:
aNm = f"<a name='T{tLine:06d}'></a>"
else:
aNm = ""
@@ -209,7 +217,7 @@ class ToHtml(Tokenizer):
if tType == self.T_EMPTY:
if parStyle is None:
parStyle = ""
if len(thisPar) > 1 and self.cssStyles:
if len(thisPar) > 1 and self._cssStyles:
parClass = " class='break'"
else:
parClass = ""
@@ -257,21 +265,21 @@ class ToHtml(Tokenizer):
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
elif tType == self.T_COMMENT and self.doComments:
elif tType == self.T_COMMENT and self._doComments:
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
elif tType == self.T_KEYWORD and self._doKeywords:
tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
tmpResult.append(tTemp)
self.theResult = "".join(tmpResult)
self._theResult = "".join(tmpResult)
tmpResult = []
if self.genMode != self.M_PREVIEW:
self.fullHTML.append(self.theResult)
if self._genMode != self.M_PREVIEW:
self._fullHTML.append(self._theResult)
return
@@ -281,7 +289,7 @@ class ToHtml(Tokenizer):
with open(savePath, mode="w", encoding="utf-8") as outFile:
theStyle = self.getStyleSheet()
theStyle.append("article {width: 800px; margin: 40px auto;}")
bodyText = "".join(self.fullHTML)
bodyText = "".join(self._fullHTML)
bodyText = bodyText.replace("\t", "&#09;").rstrip()
theHtml = (
@@ -314,24 +322,24 @@ class ToHtml(Tokenizer):
"""
htmlText = []
tabSpace = spaceChar*nSpaces
for aLine in self.fullHTML:
for aLine in self._fullHTML:
htmlText.append(aLine.replace("\t", tabSpace))
self.fullHTML = htmlText
self._fullHTML = htmlText
return
def getStyleSheet(self):
"""Generate a stylesheet appropriate for the current settings.
"""
theStyles = []
if not self.cssStyles:
if not self._cssStyles:
return theStyles
mScale = self.lineHeight/1.15
textAlign = "justify" if self.doJustify else "left"
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(
self.textFont, self.textSize
self._textFont, self._textSize
))
theStyles.append((
"p {{"
@@ -340,9 +348,9 @@ class ToHtml(Tokenizer):
"}}"
).format(
textAlign,
round(100 * self.lineHeight),
mScale * self.marginText[0],
mScale * self.marginText[1],
round(100 * self._lineHeight),
mScale * self._marginText[0],
mScale * self._marginText[1],
))
theStyles.append((
"h1 {{"
@@ -352,7 +360,7 @@ class ToHtml(Tokenizer):
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead1[0], mScale * self.marginHead1[1]
mScale * self._marginHead1[0], mScale * self._marginHead1[1]
))
theStyles.append((
"h2 {{"
@@ -362,7 +370,7 @@ class ToHtml(Tokenizer):
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead2[0], mScale * self.marginHead2[1]
mScale * self._marginHead2[0], mScale * self._marginHead2[1]
))
theStyles.append((
"h3 {{"
@@ -372,7 +380,7 @@ class ToHtml(Tokenizer):
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead3[0], mScale * self.marginHead3[1]
mScale * self._marginHead3[0], mScale * self._marginHead3[1]
))
theStyles.append((
"h4 {{"
@@ -382,7 +390,7 @@ class ToHtml(Tokenizer):
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead4[0], mScale * self.marginHead4[1]
mScale * self._marginHead4[0], mScale * self._marginHead4[1]
))
theStyles.append((
".title {{"
@@ -391,7 +399,7 @@ class ToHtml(Tokenizer):
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginTitle[0], mScale * self.marginTitle[1]
mScale * self._marginTitle[0], mScale * self._marginTitle[1]
))
theStyles.append((
".sep, .skip {{"
@@ -418,7 +426,7 @@ class ToHtml(Tokenizer):
def _formatSynopsis(self, tText):
"""Apply HTML formatting to synopsis.
"""
if self.genMode == self.M_PREVIEW:
if self._genMode == self.M_PREVIEW:
sSynop = self._trSynopsis
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {tText}</p>\n"
else:
@@ -428,7 +436,7 @@ class ToHtml(Tokenizer):
def _formatComments(self, tText):
"""Apply HTML formatting to comments.
"""
if self.genMode == self.M_PREVIEW:
if self._genMode == self.M_PREVIEW:
return f"<p class='comment'>{tText}</p>\n"
else:
sComm = self._localLookup("Comment")
@@ -449,7 +457,7 @@ class ToHtml(Tokenizer):
if theBits[0] == nwKeyWords.TAG_KEY:
retText += f"<a name='tag_{theBits[1]}'>{theBits[1]}</a>"
else:
if self.genMode == self.M_PREVIEW:
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)
+194 -178
View File
@@ -85,62 +85,62 @@ class Tokenizer():
self.mainConf = novelwriter.CONFIG
# Data Variables
self.theText = "" # The raw text to be tokenized
self.theHandle = None # The handle associated with the text
self.theItem = None # The NWItem associated with the handle
self.theTokens = [] # The list of the processed tokens
self.theResult = "" # The result of the last document
self._theText = "" # The raw text to be tokenized
self._theHandle = None # The handle associated with the text
self._theItem = None # The NWItem associated with the handle
self._theTokens = [] # The list of the processed tokens
self._theResult = "" # 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._keepMarkdown = False # Whether to keep the markdown text
self._theMarkdown = [] # The result novelWriter markdown of all documents
# User Settings
self.textFont = "Serif" # Output text font
self.textSize = 11 # Output text size
self.textFixed = False # Fixed width text
self.lineHeight = 1.15 # Line height in units of em
self.blockIndent = 4.00 # Block indent in units of em
self.doJustify = False # Justify text
self.doBodyText = True # Include body text
self.doSynopsis = False # Also process synopsis comments
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
self._textFont = "Serif" # Output text font
self._textSize = 11 # Output text size
self._textFixed = False # Fixed width text
self._lineHeight = 1.15 # Line height in units of em
self._blockIndent = 4.00 # Block indent in units of em
self._doJustify = False # Justify text
self._doBodyText = True # Include body text
self._doSynopsis = False # Also process synopsis comments
self._doComments = False # Also process comments
self._doKeywords = False # Also process keywords like tags and references
# Margins
self.marginTitle = (1.000, 0.500)
self.marginHead1 = (1.000, 0.500)
self.marginHead2 = (0.834, 0.500)
self.marginHead3 = (0.584, 0.500)
self.marginHead4 = (0.584, 0.500)
self.marginText = (0.000, 0.584)
self.marginMeta = (0.000, 0.584)
self._marginTitle = (1.000, 0.500)
self._marginHead1 = (1.000, 0.500)
self._marginHead2 = (0.834, 0.500)
self._marginHead3 = (0.584, 0.500)
self._marginHead4 = (0.584, 0.500)
self._marginText = (0.000, 0.584)
self._marginMeta = (0.000, 0.584)
# Title Formats
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
self.fmtScene = "%title%" # Formatting for scenes
self.fmtSection = "%title%" # Formatting for sections
self._fmtTitle = "%title%" # Formatting for titles
self._fmtChapter = "%title%" # Formatting for numbered chapters
self._fmtUnNum = "%title%" # Formatting for unnumbered chapters
self._fmtScene = "%title%" # Formatting for scenes
self._fmtSection = "%title%" # Formatting for sections
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
self._hideScene = False # Do not include scene headers
self._hideSection = False # Do not include section headers
self.linkHeaders = False # Add an anchor before headers
self._linkHeaders = False # Add an anchor before headers
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
self.numAbsScene = 0 # Counter for scene number within novel
self.firstScene = False # Flag to indicate that the first scene of the chapter
self._numChapter = 0 # Counter for chapter numbers
self._numChScene = 0 # Counter for scene number within chapter
self._numAbsScene = 0 # Counter for scene number within novel
self._firstScene = False # Flag to indicate that the first scene of the chapter
# This File
self.isNone = False # Document has unknown layout
self.isNovel = False # Document is a novel document
self.isNote = False # Document is a project note
self.isFirst = True # Document is the first in a set
self._isNone = False # Document has unknown layout
self._isNovel = False # Document is a novel document
self._isNote = False # Document is a project note
self._isFirst = True # Document is the first in a set
# Error Handling
self.errData = []
self._errData = []
# Function Mapping
self._localLookup = self.theProject.localLookup
@@ -151,100 +151,116 @@ class Tokenizer():
return
##
# Properties
##
@property
def theResult(self):
return self._theResult
@property
def theMarkdown(self):
return self._theMarkdown
@property
def errData(self):
return self._errData
##
# Setters
##
def setTitleFormat(self, fmtTitle):
self.fmtTitle = fmtTitle.strip()
self._fmtTitle = fmtTitle.strip()
return
def setChapterFormat(self, fmtChapter):
self.fmtChapter = fmtChapter.strip()
self._fmtChapter = fmtChapter.strip()
return
def setUnNumberedFormat(self, fmtUnNum):
self.fmtUnNum = fmtUnNum.strip()
self._fmtUnNum = fmtUnNum.strip()
return
def setSceneFormat(self, fmtScene, hideScene):
self.fmtScene = fmtScene.strip()
self.hideScene = hideScene
self._fmtScene = fmtScene.strip()
self._hideScene = hideScene
return
def setSectionFormat(self, fmtSection, hideSection):
self.fmtSection = fmtSection.strip()
self.hideSection = hideSection
self._fmtSection = fmtSection.strip()
self._hideSection = hideSection
return
def setFont(self, textFont, textSize, textFixed=False):
self.textFont = textFont
self.textSize = round(int(textSize))
self.textFixed = textFixed
self._textFont = textFont
self._textSize = round(int(textSize))
self._textFixed = textFixed
return
def setLineHeight(self, lineHeight):
self.lineHeight = min(max(float(lineHeight), 0.5), 5.0)
self._lineHeight = min(max(float(lineHeight), 0.5), 5.0)
return
def setBlockIndent(self, blockIndent):
self.blockIndent = min(max(float(blockIndent), 0.0), 10.0)
self._blockIndent = min(max(float(blockIndent), 0.0), 10.0)
return
def setJustify(self, doJustify):
self.doJustify = doJustify
self._doJustify = doJustify
return
def setTitleMargins(self, mUpper, mLower):
self.marginTitle = (float(mUpper), float(mLower))
self._marginTitle = (float(mUpper), float(mLower))
return
def setHead1Margins(self, mUpper, mLower):
self.marginHead1 = (float(mUpper), float(mLower))
self._marginHead1 = (float(mUpper), float(mLower))
return
def setHead2Margins(self, mUpper, mLower):
self.marginHead2 = (float(mUpper), float(mLower))
self._marginHead2 = (float(mUpper), float(mLower))
return
def setHead3Margins(self, mUpper, mLower):
self.marginHead3 = (float(mUpper), float(mLower))
self._marginHead3 = (float(mUpper), float(mLower))
return
def setHead4Margins(self, mUpper, mLower):
self.marginHead4 = (float(mUpper), float(mLower))
self._marginHead4 = (float(mUpper), float(mLower))
return
def setTextMargins(self, mUpper, mLower):
self.marginText = (float(mUpper), float(mLower))
self._marginText = (float(mUpper), float(mLower))
return
def setMetaMargins(self, mUpper, mLower):
self.marginMeta = (float(mUpper), float(mLower))
self._marginMeta = (float(mUpper), float(mLower))
return
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
self._linkHeaders = linkHeaders
return
def setBodyText(self, doBodyText):
self.doBodyText = doBodyText
self._doBodyText = doBodyText
return
def setSynopsis(self, doSynopsis):
self.doSynopsis = doSynopsis
self._doSynopsis = doSynopsis
return
def setComments(self, doComments):
self.doComments = doComments
self._doComments = doComments
return
def setKeywords(self, doKeywords):
self.doKeywords = doKeywords
self._doKeywords = doKeywords
return
def setKeepMarkdown(self, keepMarkdown):
self.keepMarkdown = keepMarkdown
self._keepMarkdown = keepMarkdown
return
##
@@ -261,20 +277,20 @@ class Tokenizer():
if theItem.itemType != nwItemType.ROOT:
return False
if self.isFirst:
if self._isFirst:
textAlign = self.A_CENTRE
self.isFirst = False
self._isFirst = False
else:
textAlign = self.A_PBB | self.A_CENTRE
locNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}"
self.theTokens = []
self.theTokens.append((
self._theTokens = []
self._theTokens.append((
self.T_TITLE, 0, theTitle, None, textAlign
))
if self.keepMarkdown:
self.theMarkdown.append(f"# {theTitle}\n\n")
if self._keepMarkdown:
self._theMarkdown.append(f"# {theTitle}\n\n")
return True
@@ -282,33 +298,33 @@ class Tokenizer():
"""Set the text for the tokenizer from a handle. If theText is
not set, load it from the file.
"""
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if self.theItem is None:
self._theHandle = theHandle
self._theItem = self.theProject.projTree[theHandle]
if self._theItem is None:
return False
self.theText = ""
self._theText = ""
if theText is not None:
# If the text is set, just use that
self.theText = theText
self._theText = theText
else:
# Otherwise, load it from file
theDoc = NWDoc(self.theProject, theHandle)
theText = theDoc.readDocument()
if theText:
self.theText = theText
self._theText = theText
docSize = len(self.theText)
docSize = len(self._theText)
if docSize > nwConst.MAX_DOCSIZE:
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
self.theItem.itemName, f"{docSize/1.0e6:.2f}"
self._theItem.itemName, f"{docSize/1.0e6:.2f}"
)
self.theText = "# {0}\n\n{1}\n\n".format(self.tr("ERROR"), errVal)
self.errData.append(errVal)
self._theText = "# {0}\n\n{1}\n\n".format(self.tr("ERROR"), errVal)
self._errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
self.isNovel = self.theItem.itemLayout == nwItemLayout.DOCUMENT
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
self._isNone = self._theItem.itemLayout == nwItemLayout.NO_LAYOUT
self._isNovel = self._theItem.itemLayout == nwItemLayout.DOCUMENT
self._isNote = self._theItem.itemLayout == nwItemLayout.NOTE
return True
@@ -321,11 +337,11 @@ class Tokenizer():
for aKey, aVal in self.theProject.autoReplace.items():
repDict[f"<{aKey}>"] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText)
# Process the character translation map
trDict = {nwUnicode.U_MAPOSS: nwUnicode.U_RSQUO}
self.theText = self.theText.translate(str.maketrans(trDict))
self._theText = self._theText.translate(str.maketrans(trDict))
return
@@ -341,8 +357,8 @@ class Tokenizer():
escReplace = re.compile(
"|".join([re.escape(k) for k in escapeDict.keys()]), flags=re.DOTALL
)
self.theResult = escReplace.sub(
lambda x: escapeDict[x.group(0)], self.theResult
self._theResult = escReplace.sub(
lambda x: escapeDict[x.group(0)], self._theResult
)
return
@@ -356,7 +372,7 @@ class Tokenizer():
The format of the token list is an entry with a five-tuple for
each line in the file. The tuple is as follows:
1: The type of the block, self.T_*
2: The line in file where this block occurred
2: The line in the file where this block occurred
3: The text content of the block, without leading tags
4: The internal formatting map of the text, self.FMT_*
5: The style of the block, self.A_*
@@ -368,20 +384,20 @@ class Tokenizer():
(QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
]
self.theTokens = []
self._theTokens = []
tmpMarkdown = []
nLine = 0
breakNext = False
for aLine in self.theText.splitlines():
for aLine in self._theText.splitlines():
nLine += 1
sLine = aLine.strip()
# Check for blank lines
if len(sLine) == 0:
self.theTokens.append((
self._theTokens.append((
self.T_EMPTY, nLine, "", None, self.A_NONE
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("\n")
continue
@@ -403,7 +419,7 @@ class Tokenizer():
continue
elif sLine == "[VSPACE]":
self.theTokens.append(
self._theTokens.append(
(self.T_SKIP, nLine, "", None, sAlign)
)
continue
@@ -411,11 +427,11 @@ class Tokenizer():
elif sLine.startswith("[VSPACE:") and sLine.endswith("]"):
nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1:
self.theTokens.append(
self._theTokens.append(
(self.T_SKIP, nLine, "", None, sAlign)
)
if nSkip > 1:
self.theTokens += (nSkip - 1) * [
self._theTokens += (nSkip - 1) * [
(self.T_SKIP, nLine, "", None, self.A_NONE)
]
continue
@@ -424,87 +440,87 @@ class Tokenizer():
cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower()
if synTag == "synopsis:":
self.theTokens.append((
self._theTokens.append((
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, sAlign
))
if self.doSynopsis and self.keepMarkdown:
if self._doSynopsis and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self._theTokens.append((
self.T_COMMENT, nLine, aLine[1:].strip(), None, sAlign
))
if self.doComments and self.keepMarkdown:
if self._doComments and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self._theTokens.append((
self.T_KEYWORD, nLine, aLine[1:].strip(), None, sAlign
))
if self.doKeywords and self.keepMarkdown:
if self._doKeywords and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
if self.isNovel:
if self._isNovel:
sAlign |= self.A_CENTRE
sAlign |= self.A_PBB
self.theTokens.append((
self._theTokens.append((
self.T_HEAD1, nLine, aLine[2:].strip(), None, sAlign
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
if self.isNovel:
if self._isNovel:
sAlign |= self.A_PBB
self.theTokens.append((
self._theTokens.append((
self.T_HEAD2, nLine, aLine[3:].strip(), None, sAlign
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self._theTokens.append((
self.T_HEAD3, nLine, aLine[4:].strip(), None, sAlign
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self._theTokens.append((
self.T_HEAD4, nLine, aLine[5:].strip(), None, sAlign
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "#! ":
if self.isNovel:
if self._isNovel:
tStyle = self.T_TITLE
else:
tStyle = self.T_HEAD1
self.theTokens.append((
self._theTokens.append((
tStyle, nLine, aLine[3:].strip(), None, sAlign | self.A_CENTRE
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "##! ":
if self.isNovel:
if self._isNovel:
tStyle = self.T_UNNUM
sAlign |= self.A_PBB
else:
tStyle = self.T_HEAD2
self.theTokens.append((
self._theTokens.append((
tStyle, nLine, aLine[4:].strip(), None, sAlign
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
if not self.doBodyText:
if not self._doBodyText:
# Skip all body text
continue
@@ -554,33 +570,33 @@ class Tokenizer():
# Save the line as is, but append the array of formatting locations
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
self._theTokens.append((
self.T_TEXT, nLine, aLine, fmtPos, sAlign
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
# If we have content, turn off the first page flag
if self.isFirst and self.theTokens:
self.isFirst = False
if self._isFirst and self._theTokens:
self._isFirst = False
# Make sure the token array doesn't start with a page break
# on the very first page, adding a blank first page.
if self.theTokens[0][4] & self.A_PBB:
tToken = self.theTokens[0]
self.theTokens[0] = (
if self._theTokens[0][4] & self.A_PBB:
tToken = self._theTokens[0]
self._theTokens[0] = (
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] & ~self.A_PBB
)
# Always add an empty line at the end of the file
self.theTokens.append((
self._theTokens.append((
self.T_EMPTY, nLine, "", None, self.A_NONE
))
if self.keepMarkdown:
if self._keepMarkdown:
tmpMarkdown.append("\n")
if self.keepMarkdown:
self.theMarkdown.append("".join(tmpMarkdown))
if self._keepMarkdown:
self._theMarkdown.append("".join(tmpMarkdown))
# Second Pass
# ===========
@@ -588,13 +604,13 @@ class Tokenizer():
pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
tCount = len(self.theTokens)
for n, tToken in enumerate(self.theTokens):
tCount = len(self._theTokens)
for n, tToken in enumerate(self._theTokens):
if n > 0:
pToken = self.theTokens[n-1]
pToken = self._theTokens[n-1]
if n < tCount - 1:
nToken = self.theTokens[n+1]
nToken = self._theTokens[n+1]
if tToken[0] == self.T_KEYWORD:
aStyle = tToken[4]
@@ -602,7 +618,7 @@ class Tokenizer():
aStyle |= self.A_Z_TOPMRG
if nToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_BTMMRG
self.theTokens[n] = (
self._theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
@@ -612,20 +628,20 @@ class Tokenizer():
"""Apply formatting to the text headers for novel files. This
also applies chapter and scene numbering.
"""
if not self.isNovel:
if not self._isNovel:
return False
for n, tToken in enumerate(self.theTokens):
for n, tToken in enumerate(self._theTokens):
# In case we see text before a scene, we reset the flag
if tToken[0] == self.T_TEXT:
self.firstScene = False
self._firstScene = False
elif tToken[0] == self.T_HEAD1:
# Partition
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = (
tTemp = self._formatHeading(self._fmtTitle, tToken[2])
self._theTokens[n] = (
tToken[0], tToken[1], tTemp, None, tToken[4]
)
@@ -634,75 +650,75 @@ class Tokenizer():
# Numbered or Unnumbered
if tToken[0] == self.T_UNNUM:
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
tTemp = self._formatHeading(self._fmtUnNum, tToken[2])
else:
self.numChapter += 1
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
self._numChapter += 1
tTemp = self._formatHeading(self._fmtChapter, tToken[2])
# Format the chapter header
self.theTokens[n] = (
self._theTokens[n] = (
tToken[0], tToken[1], tTemp, None, tToken[4]
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
self._firstScene = True
self._numChScene = 0
elif tToken[0] == self.T_HEAD3:
# Scene
self.numChScene += 1
self.numAbsScene += 1
self._numChScene += 1
self._numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
tTemp = self._formatHeading(self._fmtScene, tToken[2])
if tTemp == "" and self._hideScene:
self._theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
elif tTemp == "" and not self._hideScene:
if self._firstScene:
self._theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self._theTokens[n] = (
self.T_SKIP, tToken[1], "", None, tToken[4]
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
elif tTemp == self._fmtScene:
if self._firstScene:
self._theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self._theTokens[n] = (
self.T_SEP, tToken[1], tTemp, None, tToken[4] | self.A_CENTRE
)
else:
self.theTokens[n] = (
self._theTokens[n] = (
tToken[0], tToken[1], tTemp, None, tToken[4]
)
# Definitely no longer the first scene
self.firstScene = False
self._firstScene = False
elif tToken[0] == self.T_HEAD4:
# Section
tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
tTemp = self._formatHeading(self._fmtSection, tToken[2])
if tTemp == "" and self._hideSection:
self._theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
elif tTemp == "" and not self._hideSection:
self._theTokens[n] = (
self.T_SKIP, tToken[1], "", None, tToken[4]
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
elif tTemp == self._fmtSection:
self._theTokens[n] = (
self.T_SEP, tToken[1], tTemp, None, tToken[4] | self.A_CENTRE
)
else:
self.theTokens[n] = (
self._theTokens[n] = (
tToken[0], tToken[1], tTemp, None, tToken[4]
)
@@ -712,7 +728,7 @@ class Tokenizer():
"""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._theMarkdown:
outFile.write(nwdPage)
return
@@ -724,15 +740,15 @@ class Tokenizer():
"""Replaces the %keyword% strings.
"""
theTitle = theTitle.replace(r"%title%", theText)
theTitle = theTitle.replace(r"%ch%", str(self.numChapter))
theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
theTitle = theTitle.replace(r"%ch%", str(self._numChapter))
theTitle = theTitle.replace(r"%sc%", str(self._numChScene))
theTitle = theTitle.replace(r"%sca%", str(self._numAbsScene))
if r"%chw%" in theTitle:
theTitle = theTitle.replace(r"%chw%", self._localLookup(self.numChapter))
theTitle = theTitle.replace(r"%chw%", self._localLookup(self._numChapter))
if r"%chi%" in theTitle:
theTitle = theTitle.replace(r"%chi%", numberToRoman(self.numChapter, True))
theTitle = theTitle.replace(r"%chi%", numberToRoman(self._numChapter, True))
if r"%chI%" in theTitle:
theTitle = theTitle.replace(r"%chI%", numberToRoman(self.numChapter, False))
theTitle = theTitle.replace(r"%chI%", numberToRoman(self._numChapter, False))
return theTitle[:1].upper() + theTitle[1:]
+24 -16
View File
@@ -39,21 +39,29 @@ class ToMarkdown(Tokenizer):
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
self.genMode = self.M_STD
self.fullMD = []
self._genMode = self.M_STD
self._fullMD = []
return
##
# Properties
##
@property
def fullMD(self):
return self._fullMD
##
# Setters
##
def setStandardMarkdown(self):
self.genMode = self.M_STD
self._genMode = self.M_STD
return
def setGitHubMarkdown(self):
self.genMode = self.M_GH
self._genMode = self.M_GH
return
##
@@ -63,13 +71,13 @@ class ToMarkdown(Tokenizer):
def getFullResultSize(self):
"""Return the size of the full Markdown result.
"""
return sum([len(x) for x in self.fullMD])
return sum([len(x) for x in self._fullMD])
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
"""
if self.genMode == self.M_STD:
if self._genMode == self.M_STD:
# Standard
mdTags = {
self.FMT_B_B: "**",
@@ -90,12 +98,12 @@ class ToMarkdown(Tokenizer):
self.FMT_D_E: "~~",
}
self.theResult = ""
self._theResult = ""
thisPar = []
tmpResult = []
for tType, _, tText, tFormat, tStyle in self.theTokens:
for tType, _, tText, tFormat, tStyle in self._theTokens:
# Process Text Type
if tType == self.T_EMPTY:
@@ -140,21 +148,21 @@ class ToMarkdown(Tokenizer):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
elif tType == self.T_SYNOPSIS and self._doSynopsis:
locName = self._localLookup("Synopsis")
tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_COMMENT and self.doComments:
elif tType == self.T_COMMENT and self._doComments:
locName = self._localLookup("Comment")
tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_KEYWORD and self.doKeywords:
elif tType == self.T_KEYWORD and self._doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
self.theResult = "".join(tmpResult)
self._theResult = "".join(tmpResult)
tmpResult = []
self.fullMD.append(self.theResult)
self._fullMD.append(self._theResult)
return
@@ -162,7 +170,7 @@ class ToMarkdown(Tokenizer):
"""Save the data to a plain text file.
"""
with open(savePath, mode="w", encoding="utf-8") as outFile:
theText = "".join(self.fullMD)
theText = "".join(self._fullMD)
outFile.write(theText)
return
@@ -172,10 +180,10 @@ class ToMarkdown(Tokenizer):
"""
fullMD = []
eightSpace = spaceChar*nSpaces
for aPage in self.fullMD:
for aPage in self._fullMD:
fullMD.append(aPage.replace("\t", eightSpace))
self.fullMD = fullMD
self._fullMD = fullMD
return
##
+77 -77
View File
@@ -115,27 +115,27 @@ class ToOdt(Tokenizer):
self._errData = [] # List of errors encountered
# Properties
self.textFont = "Liberation Serif"
self.textSize = 12
self.textFixed = False
self.colourHead = False
self.headerText = ""
self._textFont = "Liberation Serif"
self._textSize = 12
self._textFixed = False
self._colourHead = False
self._headerText = ""
# Internal
self._fontFamily = "&apos;Liberation Serif&apos;"
self._fontPitch = "variable"
self._fSizeTitle = "30pt"
self._fSizeHead1 = "24pt"
self._fSizeHead2 = "20pt"
self._fSizeHead3 = "16pt"
self._fSizeHead4 = "14pt"
self._fSizeHead = "14pt"
self._fSizeText = "12pt"
self._lineHeight = "115%"
self._blockIndent = "1.693cm"
self._textAlign = "left"
self._dLanguage = "en"
self._dCountry = "GB"
self._fontFamily = "&apos;Liberation Serif&apos;"
self._fontPitch = "variable"
self._fSizeTitle = "30pt"
self._fSizeHead1 = "24pt"
self._fSizeHead2 = "20pt"
self._fSizeHead3 = "16pt"
self._fSizeHead4 = "14pt"
self._fSizeHead = "14pt"
self._fSizeText = "12pt"
self._fLineHeight = "115%"
self._fBlockIndent = "1.693cm"
self._textAlign = "left"
self._dLanguage = "en"
self._dCountry = "GB"
# Text Margings in Units of em
self._mTopTitle = "0.423cm"
@@ -192,7 +192,7 @@ class ToOdt(Tokenizer):
def setColourHeaders(self, doColour):
"""Enable/disable coloured headings and comments.
"""
self.colourHead = doColour
self._colourHead = doColour
return
##
@@ -209,40 +209,40 @@ class ToOdt(Tokenizer):
# Initialise Variables
# ====================
self._fontFamily = self.textFont
if len(self.textFont.split()) > 1:
self._fontFamily = f"'{self.textFont}'"
self._fontPitch = "fixed" if self.textFixed else "variable"
self._fontFamily = self._textFont
if len(self._textFont.split()) > 1:
self._fontFamily = f"'{self._textFont}'"
self._fontPitch = "fixed" if self._textFixed else "variable"
self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt"
self._fSizeHead1 = f"{round(2.00 * self.textSize):d}pt"
self._fSizeHead2 = f"{round(1.60 * self.textSize):d}pt"
self._fSizeHead3 = f"{round(1.30 * self.textSize):d}pt"
self._fSizeHead4 = f"{round(1.15 * self.textSize):d}pt"
self._fSizeHead = f"{round(1.15 * self.textSize):d}pt"
self._fSizeText = f"{self.textSize:d}pt"
self._fSizeTitle = f"{round(2.50 * self._textSize):d}pt"
self._fSizeHead1 = f"{round(2.00 * self._textSize):d}pt"
self._fSizeHead2 = f"{round(1.60 * self._textSize):d}pt"
self._fSizeHead3 = f"{round(1.30 * self._textSize):d}pt"
self._fSizeHead4 = f"{round(1.15 * self._textSize):d}pt"
self._fSizeHead = f"{round(1.15 * self._textSize):d}pt"
self._fSizeText = f"{self._textSize:d}pt"
mScale = self.lineHeight/1.15
mScale = self._lineHeight/1.15
self._mTopTitle = self._emToCm(mScale * self.marginTitle[0])
self._mTopHead1 = self._emToCm(mScale * self.marginHead1[0])
self._mTopHead2 = self._emToCm(mScale * self.marginHead2[0])
self._mTopHead3 = self._emToCm(mScale * self.marginHead3[0])
self._mTopHead4 = self._emToCm(mScale * self.marginHead4[0])
self._mTopHead = self._emToCm(mScale * self.marginHead4[0])
self._mTopText = self._emToCm(mScale * self.marginText[0])
self._mTopMeta = self._emToCm(mScale * self.marginMeta[0])
self._mTopTitle = self._emToCm(mScale * self._marginTitle[0])
self._mTopHead1 = self._emToCm(mScale * self._marginHead1[0])
self._mTopHead2 = self._emToCm(mScale * self._marginHead2[0])
self._mTopHead3 = self._emToCm(mScale * self._marginHead3[0])
self._mTopHead4 = self._emToCm(mScale * self._marginHead4[0])
self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
self._mTopText = self._emToCm(mScale * self._marginText[0])
self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
self._mBotTitle = self._emToCm(mScale * self.marginTitle[1])
self._mBotHead1 = self._emToCm(mScale * self.marginHead1[1])
self._mBotHead2 = self._emToCm(mScale * self.marginHead2[1])
self._mBotHead3 = self._emToCm(mScale * self.marginHead3[1])
self._mBotHead4 = self._emToCm(mScale * self.marginHead4[1])
self._mBotHead = self._emToCm(mScale * self.marginHead4[1])
self._mBotText = self._emToCm(mScale * self.marginText[1])
self._mBotMeta = self._emToCm(mScale * self.marginMeta[1])
self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
self._mBotHead2 = self._emToCm(mScale * self._marginHead2[1])
self._mBotHead3 = self._emToCm(mScale * self._marginHead3[1])
self._mBotHead4 = self._emToCm(mScale * self._marginHead4[1])
self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
self._mBotText = self._emToCm(mScale * self._marginText[1])
self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
if self.colourHead:
if self._colourHead:
self._colHead12 = "#2a6099"
self._opaHead12 = "100%"
self._colHead34 = "#444444"
@@ -250,9 +250,9 @@ class ToOdt(Tokenizer):
self._colMetaTx = "#813709"
self._opaMetaTx = "100%"
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
self._blockIndent = self._emToCm(self.blockIndent)
self._textAlign = "justify" if self.doJustify else "left"
self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
self._fBlockIndent = self._emToCm(self._blockIndent)
self._textAlign = "justify" if self._doJustify else "left"
# Clear Errors
self._errData = []
@@ -260,10 +260,10 @@ class ToOdt(Tokenizer):
# Document Header
# ===============
if self.headerText == "":
if self._headerText == "":
theTitle = self.theProject.bookTitle
theAuth = self.theProject.getAuthors()
self.headerText = f"{theTitle} / {theAuth} /"
self._headerText = f"{theTitle} / {theAuth} /"
# Create Roots
# ============
@@ -272,7 +272,7 @@ class ToOdt(Tokenizer):
tAttr[_mkTag("office", "version")] = X_VERS
fAttr = {}
fAttr[_mkTag("style", "name")] = self.textFont
fAttr[_mkTag("style", "name")] = self._textFont
fAttr[_mkTag("style", "font-pitch")] = self._fontPitch
if self._isFlat:
@@ -345,7 +345,7 @@ class ToOdt(Tokenizer):
def doConvert(self):
"""Convert the list of text tokens into XML elements.
"""
self.theResult = "" # Not used, but cleared just in case
self._theResult = "" # Not used, but cleared just in case
odtTags = {
self.FMT_B_B: "_B", # Bold open format
@@ -359,7 +359,7 @@ class ToOdt(Tokenizer):
thisPar = []
thisFmt = []
parStyle = None
for tType, _, tText, tFormat, tStyle in self.theTokens:
for tType, _, tText, tFormat, tStyle in self._theTokens:
# Styles
oStyle = ODTParagraphStyle()
@@ -385,14 +385,14 @@ class ToOdt(Tokenizer):
oStyle.setMarginTop("0.000cm")
if tStyle & self.A_IND_L:
oStyle.setMarginLeft(self._blockIndent)
oStyle.setMarginLeft(self._fBlockIndent)
if tStyle & self.A_IND_R:
oStyle.setMarginRight(self._blockIndent)
oStyle.setMarginRight(self._fBlockIndent)
# Process Text Types
if tType == self.T_EMPTY:
if len(thisPar) > 1 and parStyle is not None:
if self.doJustify:
if self._doJustify:
parStyle.setTextAlign("left")
if len(thisPar) > 0:
@@ -449,15 +449,15 @@ class ToOdt(Tokenizer):
thisPar.append(tTxt)
thisFmt.append(tFmt)
elif tType == self.T_SYNOPSIS and self.doSynopsis:
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
elif tType == self.T_COMMENT and self.doComments:
elif tType == self.T_COMMENT and self._doComments:
tTemp, fTemp = self._formatComments(tText)
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
elif tType == self.T_KEYWORD and self.doKeywords:
elif tType == self.T_KEYWORD and self._doKeywords:
tTemp, fTemp = self._formatKeywords(tText)
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
@@ -696,7 +696,7 @@ class ToOdt(Tokenizer):
def _emToCm(self, emVal):
"""Converts an em value to centimetres.
"""
return f"{emVal*2.54/72*self.textSize:.3f}cm"
return f"{emVal*2.54/72*self._textSize:.3f}cm"
##
# Style Elements
@@ -747,7 +747,7 @@ class ToOdt(Tokenizer):
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self.textFont
theAttr[_mkTag("style", "font-name")] = self._textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
theAttr[_mkTag("fo", "language")] = self._dLanguage
@@ -764,7 +764,7 @@ class ToOdt(Tokenizer):
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self.textFont
theAttr[_mkTag("style", "font-name")] = self._textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
@@ -787,7 +787,7 @@ class ToOdt(Tokenizer):
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self.textFont
theAttr[_mkTag("style", "font-name")] = self._textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeHead
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
@@ -816,8 +816,8 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopText)
oStyle.setMarginBottom(self._mBotText)
oStyle.setLineHeight(self._lineHeight)
oStyle.setFontName(self.textFont)
oStyle.setLineHeight(self._fLineHeight)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeText)
oStyle.setTextAlign(self._textAlign)
@@ -834,8 +834,8 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopMeta)
oStyle.setMarginBottom(self._mBotMeta)
oStyle.setLineHeight(self._lineHeight)
oStyle.setFontName(self.textFont)
oStyle.setLineHeight(self._fLineHeight)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeText)
oStyle.setColor(self._colMetaTx)
@@ -855,7 +855,7 @@ class ToOdt(Tokenizer):
oStyle.setTextAlign("center")
oStyle.setMarginTop(self._mTopTitle)
oStyle.setMarginBottom(self._mBotTitle)
oStyle.setFontName(self.textFont)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeTitle)
oStyle.setFontWeight("bold")
@@ -874,7 +874,7 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead1)
oStyle.setMarginBottom(self._mBotHead1)
oStyle.setFontName(self.textFont)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead1)
oStyle.setColor(self._colHead12)
@@ -895,7 +895,7 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead2)
oStyle.setMarginBottom(self._mBotHead2)
oStyle.setFontName(self.textFont)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead2)
oStyle.setColor(self._colHead12)
@@ -916,7 +916,7 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead3)
oStyle.setMarginBottom(self._mBotHead3)
oStyle.setFontName(self.textFont)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead3)
oStyle.setColor(self._colHead34)
@@ -937,7 +937,7 @@ class ToOdt(Tokenizer):
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead4)
oStyle.setMarginBottom(self._mBotHead4)
oStyle.setFontName(self.textFont)
oStyle.setFontName(self._textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead4)
oStyle.setColor(self._colHead34)
@@ -972,7 +972,7 @@ class ToOdt(Tokenizer):
xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={
_mkTag("text", "style-name"): "Header"
})
xPar.text = self.headerText.strip() + " "
xPar.text = self._headerText.strip() + " "
xTail = etree.SubElement(xPar, _mkTag("text", "page-number"), attrib={
_mkTag("text", "select-page"): "current"