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
+7 -11
View File
@@ -110,7 +110,7 @@ CONFIG = Config()
def main(sysArgs=None):
"""Parse command line, set up logging, and launches main GUI.
"""Parse command line, set up logging, and launch main GUI.
"""
if sysArgs is None:
sysArgs = sys.argv[1:]
@@ -130,8 +130,8 @@ def main(sysArgs=None):
]
helpMsg = (
"novelWriter {version} ({date})\n"
"{copyright}\n"
f"novelWriter {__version__} ({__date__})\n"
f"{__copyright__}\n"
"\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
@@ -147,10 +147,6 @@ def main(sysArgs=None):
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
).format(
version=__version__,
copyright=__copyright__,
date=__date__,
)
# Defaults
@@ -165,9 +161,9 @@ def main(sysArgs=None):
# Parse Options
try:
inOpts, inRemain = getopt.getopt(sysArgs, shortOpt, longOpt)
except getopt.GetoptError as E:
except getopt.GetoptError as exc:
print(helpMsg)
print("ERROR: %s" % str(E))
print(f"ERROR: {str(exc)}")
sys.exit(2)
if len(inRemain) > 0:
@@ -268,7 +264,7 @@ def main(sysArgs=None):
elif CONFIG.osWindows:
try:
import ctypes
appID = "io.novelwriter.%s" % __version__
appID = f"io.novelwriter.{__version__}"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appID)
except Exception:
logger.error("Failed to set application name")
@@ -281,7 +277,7 @@ def main(sysArgs=None):
return nwGUI
else:
nwApp = QApplication([CONFIG.appName, ("-style=%s" % qtStyle)])
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
nwApp.setApplicationName(CONFIG.appName)
nwApp.setApplicationVersion(__version__)
nwApp.setWindowIcon(QIcon(CONFIG.appIcon))
+1 -1
View File
@@ -31,8 +31,8 @@ import logging
from datetime import datetime
from configparser import ConfigParser
from PyQt5.QtWidgets import qApp
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import qApp
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException
+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"
+21 -19
View File
@@ -120,27 +120,29 @@ class NWErrorMessage(QDialog):
kernelVersion = "Unknown"
try:
import lxml
lxmlVersion = lxml.__version__
except Exception:
lxmlVersion = "Unknown"
try:
import enchant
enchantVersion = enchant.__version__
except Exception:
enchantVersion = "Unknown"
try:
exTrace = "\n".join(format_tb(exTrace))
self.msgBody.setPlainText((
"Environment:\n"
"novelWriter Version: {nwVersion}\n"
"Host OS: {osType} ({osKernel})\n"
"Python: {pyVersion} ({pyHexVer:#x})\n"
"Qt: {qtVers}, PyQt: {pyqtVers}\n"
"\n"
"{exType}:\n{exMessage}\n"
"\n"
"Traceback:\n{exTrace}\n"
).format(
nwVersion=__version__,
osType=sys.platform,
osKernel=kernelVersion,
pyVersion=sys.version.split()[0],
pyHexVer=sys.hexversion,
qtVers=QT_VERSION_STR,
pyqtVers=PYQT_VERSION_STR,
exType=exType.__name__,
exMessage=str(exValue),
exTrace="\n".join(format_tb(exTrace)),
f"novelWriter Version: {__version__}\n"
f"Host OS: {sys.platform} ({kernelVersion})\n"
f"Python: {sys.version.split()[0]} ({sys.hexversion:#x})\n"
f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n"
f"lxml: {lxmlVersion}\n"
f"enchant: {enchantVersion}\n\n"
f"{exType.__name__}:\n{str(exValue)}\n\n"
f"Traceback:\n{exTrace}\n"
))
except Exception:
self.msgBody.setPlainText("Failed to generate error report ...")
+3
View File
@@ -44,6 +44,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
# Not a valid handle
theDoc = NWDoc(theProject, "stuff")
assert bool(theDoc) is False
assert theDoc.readDocument() is None
# Non-existent handle
@@ -67,6 +68,8 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
assert nHandle is not None
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
theDoc = NWDoc(theProject, xHandle)
assert bool(theDoc) is True
assert repr(theDoc) == f"<NWDoc handle={xHandle}>"
assert theDoc.readDocument() == ""
# Write Document
+3
View File
@@ -219,6 +219,9 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI):
assert theIndex.notesChangedSince(0) is True
assert theIndex.indexChangedSince(0) is True
assert theIndex.getHandleHeaderLevel(cHandle) == "H1"
assert theIndex.getHandleHeaderLevel(nHandle) == "H1"
# Zero Items
assert theIndex.checkThese([], cItem) == []
+59 -46
View File
@@ -48,36 +48,39 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
theProject.projTree.setSeed(42)
# Setting no data should fail
assert not theProject.newProject({})
assert theProject.newProject({}) is False
# Wrong type should also fail
assert theProject.newProject("stuff") is False
# Try again with a proper path
assert theProject.newProject({"projPath": fncDir})
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.newProject({"projPath": fncDir}) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
# Creating the project once more should fail
assert not theProject.newProject({"projPath": fncDir})
assert theProject.newProject({"projPath": fncDir}) is False
# Check the new project
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
# Open again
assert theProject.openProject(projFile)
assert theProject.openProject(projFile) is True
# Save and close
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert not theProject.projChanged
assert theProject.projChanged is False
# Open a second time
assert theProject.openProject(projFile)
assert not theProject.openProject(projFile)
assert theProject.openProject(projFile, overrideLock=True)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.openProject(projFile) is True
assert theProject.openProject(projFile) is False
assert theProject.openProject(projFile, overrideLock=True) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
@@ -116,9 +119,9 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI):
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.newProject(projData) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
@@ -158,9 +161,9 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI):
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.newProject(projData) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
@@ -207,11 +210,11 @@ def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir):
srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile)
assert theProject.newProject(projData)
assert theProject.openProject(fncDir)
assert theProject.newProject(projData) is True
assert theProject.openProject(fncDir) is True
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.saveProject() is True
assert theProject.closeProject() is True
os.unlink(dstSample)
# END Test testCoreProject_NewSampleA
@@ -242,11 +245,11 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir):
assert not theProject.newProject(projData)
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx")
assert theProject.newProject(projData)
assert theProject.openProject(fncDir)
assert theProject.newProject(projData) is True
assert theProject.openProject(fncDir) is True
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.saveProject() is True
assert theProject.closeProject() is True
# Misdirect the appRoot path so neither is possible
tmpConf.appRoot = tmpDir
@@ -266,11 +269,11 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject({"projPath": fncDir})
assert theProject.setProjectPath(fncDir)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.openProject(projFile)
assert theProject.newProject({"projPath": fncDir}) is True
assert theProject.setProjectPath(fncDir) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
@@ -281,13 +284,13 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
assert theProject.projChanged
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.projChanged is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert not theProject.projChanged
assert theProject.projChanged is False
# END Test testCoreProject_NewRoot
@@ -303,21 +306,21 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI):
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject({"projPath": fncDir})
assert theProject.setProjectPath(fncDir)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.openProject(projFile)
assert theProject.newProject({"projPath": fncDir}) is True
assert theProject.setProjectPath(fncDir) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
assert theProject.projChanged
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert not theProject.projChanged
assert theProject.projChanged is False
# END Test testCoreProject_NewFile
@@ -466,6 +469,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
"""
theProject = NWProject(mockGUI)
testFile = os.path.join(nwMinimal, "nwProject.nwx")
backFile = os.path.join(nwMinimal, "nwProject.bak")
compFile = os.path.join(refDir, os.path.pardir, "minimal", "nwProject.nwx")
# Nothing to save
@@ -476,7 +480,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
# Fail on folder structure check
with monkeypatch.context() as mp:
mp.setattr("os.path.isdir", lambda *args: False)
mp.setattr("os.path.isdir", lambda *a: False)
assert theProject.saveProject() is False
# Fail on open file
@@ -484,6 +488,12 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
mp.setattr("builtins.open", causeOSError)
assert theProject.saveProject() is False
# Fail on creating .bak file
with monkeypatch.context() as mp:
mp.setattr("os.replace", causeOSError)
assert theProject.saveProject() is False
assert os.path.isfile(backFile) is False
# Successful save
saveCount = theProject.saveCount
autoCount = theProject.autoCount
@@ -492,6 +502,9 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
assert theProject.autoCount == autoCount
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
# Check that a second save creates a .bak file
assert os.path.isfile(backFile) is True
# Successful autosave
saveCount = theProject.saveCount
autoCount = theProject.autoCount
+52 -52
View File
@@ -38,12 +38,12 @@ def testCoreToHtml_ConvertFormat(mockGUI):
# Novel Files Headers
# ===================
theHtml.isNovel = True
theHtml.isNote = False
theHtml.isFirst = True
theHtml._isNovel = True
theHtml._isNote = False
theHtml._isFirst = True
# Header 1
theHtml.theText = "# Partition\n"
theHtml._theText = "# Partition\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -51,7 +51,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Header 2
theHtml.theText = "## Chapter Title\n"
theHtml._theText = "## Chapter Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -59,19 +59,19 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Header 3
theHtml.theText = "### Scene Title\n"
theHtml._theText = "### Scene Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h2>Scene Title</h2>\n"
# Header 4
theHtml.theText = "#### Section Title\n"
theHtml._theText = "#### Section Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h3>Section Title</h3>\n"
# Title
theHtml.theText = "#! Title\n"
theHtml._theText = "#! Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -79,7 +79,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Unnumbered
theHtml.theText = "##! Prologue\n"
theHtml._theText = "##! Prologue\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h1 style='page-break-before: always;'>Prologue</h1>\n"
@@ -87,37 +87,37 @@ def testCoreToHtml_ConvertFormat(mockGUI):
# Note Files Headers
# ==================
theHtml.isNovel = False
theHtml.isNote = True
theHtml.isFirst = True
theHtml._isNovel = False
theHtml._isNote = True
theHtml._isFirst = True
theHtml.setLinkHeaders(True)
# Header 1
theHtml.theText = "# Heading One\n"
theHtml._theText = "# Heading One\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h1><a name='T000001'></a>Heading One</h1>\n"
# Header 2
theHtml.theText = "## Heading Two\n"
theHtml._theText = "## Heading Two\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h2><a name='T000001'></a>Heading Two</h2>\n"
# Header 3
theHtml.theText = "### Heading Three\n"
theHtml._theText = "### Heading Three\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h3><a name='T000001'></a>Heading Three</h3>\n"
# Header 4
theHtml.theText = "#### Heading Four\n"
theHtml._theText = "#### Heading Four\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h4><a name='T000001'></a>Heading Four</h4>\n"
# Title
theHtml.theText = "#! Heading One\n"
theHtml._theText = "#! Heading One\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -125,7 +125,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Unnumbered
theHtml.theText = "##! Heading Two\n"
theHtml._theText = "##! Heading Two\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h2><a name='T000001'></a>Heading Two</h2>\n"
@@ -134,7 +134,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
# ==========
# Text
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theHtml._theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -143,7 +143,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Text w/Hard Break
theHtml.theText = "Line one \nLine two \nLine three\n"
theHtml._theText = "Line one \nLine two \nLine three\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -151,13 +151,13 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Synopsis
theHtml.theText = "%synopsis: The synopsis ...\n"
theHtml._theText = "%synopsis: The synopsis ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == ""
theHtml.setSynopsis(True)
theHtml.theText = "%synopsis: The synopsis ...\n"
theHtml._theText = "%synopsis: The synopsis ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -165,13 +165,13 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Comment
theHtml.theText = "% A comment ...\n"
theHtml._theText = "% A comment ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == ""
theHtml.setComments(True)
theHtml.theText = "% A comment ...\n"
theHtml._theText = "% A comment ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -179,13 +179,13 @@ def testCoreToHtml_ConvertFormat(mockGUI):
)
# Keywords
theHtml.theText = "@char: Bod, Jane\n"
theHtml._theText = "@char: Bod, Jane\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == ""
theHtml.setKeywords(True)
theHtml.theText = "@char: Bod, Jane\n"
theHtml._theText = "@char: Bod, Jane\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -195,7 +195,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
# Multiple Keywords
theHtml.setKeywords(True)
theHtml.theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
theHtml._theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -218,7 +218,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
theHtml.setPreview(True, True)
# Text (HTML4)
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theHtml._theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -238,15 +238,15 @@ def testCoreToHtml_ConvertDirect(mockGUI):
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
theHtml.isNovel = True
theHtml.isNote = False
theHtml._isNovel = True
theHtml._isNote = False
theHtml.setLinkHeaders(True)
# Special Titles
# ==============
# Title
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB | theHtml.A_CENTRE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
@@ -257,7 +257,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
)
# Unnumbered
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_UNNUM, 1, "Prologue", None, theHtml.A_PBB),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
@@ -271,7 +271,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# ==========
# Separator
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_SEP, 1, "* * *", None, theHtml.A_CENTRE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
@@ -279,7 +279,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
assert theHtml.theResult == "<p class='sep' style='text-align: center;'>* * *</p>\n"
# Skip
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_SKIP, 1, "", None, theHtml.A_NONE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
@@ -293,7 +293,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Left
theHtml.setStyles(False)
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
]
theHtml.doConvert()
@@ -304,7 +304,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
theHtml.setStyles(True)
# Align Left
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
]
theHtml.doConvert()
@@ -313,7 +313,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
)
# Align Right
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_RIGHT),
]
theHtml.doConvert()
@@ -322,7 +322,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
)
# Align Centre
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_CENTRE),
]
theHtml.doConvert()
@@ -331,7 +331,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
)
# Align Justify
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_JUSTIFY),
]
theHtml.doConvert()
@@ -343,7 +343,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# ==========
# Page Break Always
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB | theHtml.A_PBA),
]
theHtml.doConvert()
@@ -356,7 +356,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# ======
# Indent Left
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_TEXT, 1, "Some text ...", [], theHtml.A_IND_L),
(theHtml.T_EMPTY, 2, "", None, theHtml.A_NONE),
]
@@ -366,7 +366,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
)
# Indent Right
theHtml.theTokens = [
theHtml._theTokens = [
(theHtml.T_TEXT, 1, "Some text ...", [], theHtml.A_IND_R),
(theHtml.T_EMPTY, 2, "", None, theHtml.A_NONE),
]
@@ -384,33 +384,33 @@ def testCoreToHtml_SpecialCases(mockGUI):
"""
theProject = NWProject(mockGUI)
theHtml = ToHtml(theProject)
theHtml.isNovel = True
theHtml._isNovel = True
# Greater/Lesser than symbols
# ===========================
theHtml.theText = "Text with > and < with some **bold text** in it.\n"
theHtml._theText = "Text with > and < with some **bold text** in it.\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Text with &gt; and &lt; with some <strong>bold text</strong> in it.</p>\n"
)
theHtml.theText = "Text with some <**bold text**> in it.\n"
theHtml._theText = "Text with some <**bold text**> in it.\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Text with some &lt;<strong>bold text</strong>&gt; in it.</p>\n"
)
theHtml.theText = "Let's > be > _difficult **shall** > we_?\n"
theHtml._theText = "Let's > be > _difficult **shall** > we_?\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Let's &gt; be &gt; <em>difficult <strong>shall</strong> &gt; we</em>?</p>\n"
)
theHtml.theText = "Test > text _<**bold**>_ and more.\n"
theHtml._theText = "Test > text _<**bold**>_ and more.\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
@@ -426,7 +426,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir):
"""
theProject = NWProject(mockGUI)
theHtml = ToHtml(theProject)
theHtml.isNovel = True
theHtml._isNovel = True
# Build Project
# =============
@@ -472,7 +472,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir):
]
for i in range(len(docText)):
theHtml.theText = docText[i]
theHtml._theText = docText[i]
theHtml.doPreProcessing()
theHtml.tokenizeText()
theHtml.doConvert()
@@ -526,7 +526,7 @@ def testCoreToHtml_Methods(mockGUI):
# Auto-Replace, keep Unicode
docText = "Text with <brackets> & shortdash, long—dash …\n"
theHtml.theText = docText
theHtml._theText = docText
theHtml.setReplaceUnicode(False)
theHtml.doPreProcessing()
theHtml.tokenizeText()
@@ -537,7 +537,7 @@ def testCoreToHtml_Methods(mockGUI):
# Auto-Replace, replace Unicode
docText = "Text with <brackets> & shortdash, long—dash …\n"
theHtml.theText = docText
theHtml._theText = docText
theHtml.setReplaceUnicode(True)
theHtml.doPreProcessing()
theHtml.tokenizeText()
@@ -548,7 +548,7 @@ def testCoreToHtml_Methods(mockGUI):
# With Preview
theHtml.setPreview(True, True)
theHtml.theText = docText
theHtml._theText = docText
theHtml.doPreProcessing()
theHtml.tokenizeText()
theHtml.doConvert()
File diff suppressed because it is too large Load Diff
+27 -27
View File
@@ -38,42 +38,42 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
# Headers
# =======
theMD.isNovel = True
theMD.isNote = False
theMD.isFirst = True
theMD._isNovel = True
theMD._isNote = False
theMD._isFirst = True
# Header 1
theMD.theText = "# Partition\n"
theMD._theText = "# Partition\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "# Partition\n\n"
# Header 2
theMD.theText = "## Chapter Title\n"
theMD._theText = "## Chapter Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "## Chapter Title\n\n"
# Header 3
theMD.theText = "### Scene Title\n"
theMD._theText = "### Scene Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "### Scene Title\n\n"
# Header 4
theMD.theText = "#### Section Title\n"
theMD._theText = "#### Section Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "#### Section Title\n\n"
# Title
theMD.theText = "#! Title\n"
theMD._theText = "#! Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "# Title\n\n"
# Unnumbered
theMD.theText = "##! Prologue\n"
theMD._theText = "##! Prologue\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "## Prologue\n\n"
@@ -83,7 +83,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
# Text for GitHub Markdown
theMD.setGitHubMarkdown()
theMD.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD._theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == (
@@ -92,7 +92,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
# Text for Standard Markdown
theMD.setStandardMarkdown()
theMD.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD._theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == (
@@ -100,50 +100,50 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
)
# Text w/Hard Break
theMD.theText = "Line one \nLine two \nLine three\n"
theMD._theText = "Line one \nLine two \nLine three\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "Line one \nLine two \nLine three\n\n"
# Synopsis
theMD.theText = "%synopsis: The synopsis ...\n"
theMD._theText = "%synopsis: The synopsis ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == ""
theMD.setSynopsis(True)
theMD.theText = "%synopsis: The synopsis ...\n"
theMD._theText = "%synopsis: The synopsis ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Synopsis:** The synopsis ...\n\n"
# Comment
theMD.theText = "% A comment ...\n"
theMD._theText = "% A comment ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == ""
theMD.setComments(True)
theMD.theText = "% A comment ...\n"
theMD._theText = "% A comment ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Comment:** A comment ...\n\n"
# Keywords
theMD.theText = "@char: Bod, Jane\n"
theMD._theText = "@char: Bod, Jane\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == ""
theMD.setKeywords(True)
theMD.theText = "@char: Bod, Jane\n"
theMD._theText = "@char: Bod, Jane\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Characters:** Bod, Jane\n\n"
# Multiple Keywords
theMD.setKeywords(True)
theMD.theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
theMD._theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == (
@@ -164,14 +164,14 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
theMD.isNovel = True
theMD.isNote = False
theMD._isNovel = True
theMD._isNote = False
# Special Titles
# ==============
# Title
theMD.theTokens = [
theMD._theTokens = [
(theMD.T_TITLE, 1, "A Title", None, theMD.A_PBB | theMD.A_CENTRE),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
@@ -179,7 +179,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
assert theMD.theResult == "# A Title\n\n"
# Unnumbered
theMD.theTokens = [
theMD._theTokens = [
(theMD.T_UNNUM, 1, "Prologue", None, theMD.A_PBB),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
@@ -190,7 +190,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
# ==========
# Separator
theMD.theTokens = [
theMD._theTokens = [
(theMD.T_SEP, 1, "* * *", None, theMD.A_CENTRE),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
@@ -198,7 +198,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
assert theMD.theResult == "* * *\n\n"
# Skip
theMD.theTokens = [
theMD._theTokens = [
(theMD.T_SKIP, 1, "", None, theMD.A_NONE),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
@@ -214,7 +214,7 @@ def testCoreToMarkdown_Complex(mockGUI, fncDir):
"""
theProject = NWProject(mockGUI)
theMD = ToMarkdown(theProject)
theMD.isNovel = True
theMD._isNovel = True
# Build Project
# =============
@@ -239,7 +239,7 @@ def testCoreToMarkdown_Complex(mockGUI, fncDir):
]
for i in range(len(docText)):
theMD.theText = docText[i]
theMD._theText = docText[i]
theMD.doPreProcessing()
theMD.tokenizeText()
theMD.doConvert()
+25 -25
View File
@@ -236,7 +236,7 @@ def testCoreToOdt_Convert(mockGUI):
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc.isNovel = True
theDoc._isNovel = True
def getStyle(styleName):
for aSet in theDoc._autoPara.values():
@@ -248,7 +248,7 @@ def testCoreToOdt_Convert(mockGUI):
# =======
# Header 1
theDoc.theText = "# Title\n"
theDoc._theText = "# Title\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -261,7 +261,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Header 2
theDoc.theText = "## Chapter\n"
theDoc._theText = "## Chapter\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -274,7 +274,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Header 3
theDoc.theText = "### Scene\n"
theDoc._theText = "### Scene\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -287,7 +287,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Header 4
theDoc.theText = "#### Section\n"
theDoc._theText = "#### Section\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -300,7 +300,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Title
theDoc.theText = "#! Title\n"
theDoc._theText = "#! Title\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -313,7 +313,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Unnumbered chapter
theDoc.theText = "##! Prologue\n"
theDoc._theText = "##! Prologue\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -329,7 +329,7 @@ def testCoreToOdt_Convert(mockGUI):
# ==========
# Nested Text
theDoc.theText = "Some ~~nested **bold** and _italics_ text~~ text."
theDoc._theText = "Some ~~nested **bold** and _italics_ text~~ text."
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -347,7 +347,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Hard Break
theDoc.theText = "Some text.\nNext line\n"
theDoc._theText = "Some text.\nNext line\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -360,7 +360,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Tab
theDoc.theText = "\tItem 1\tItem 2\n"
theDoc._theText = "\tItem 1\tItem 2\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -373,7 +373,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Tab in Format
theDoc.theText = "Some **bold\ttext**"
theDoc._theText = "Some **bold\ttext**"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
@@ -387,7 +387,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Multiple Spaces
theDoc.theText = (
theDoc._theText = (
"### Scene\n\n"
"Hello World\n\n"
"Hello World\n\n"
@@ -408,7 +408,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Synopsis, Comment, Keywords
theDoc.theText = (
theDoc._theText = (
"### Scene\n\n"
"@pov: Jane\n\n"
"% synopsis: So it begins\n\n"
@@ -435,7 +435,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Scene Separator
theDoc.theText = "### Scene One\n\nText\n\n### Scene Two\n\nText"
theDoc._theText = "### Scene One\n\nText\n\n### Scene Two\n\nText"
theDoc.setSceneFormat("* * *", False)
theDoc.tokenizeText()
theDoc.doHeaders()
@@ -453,7 +453,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Scene Break
theDoc.theText = "### Scene One\n\nText\n\n### Scene Two\n\nText"
theDoc._theText = "### Scene One\n\nText\n\n### Scene Two\n\nText"
theDoc.setSceneFormat("", False)
theDoc.tokenizeText()
theDoc.doHeaders()
@@ -471,7 +471,7 @@ def testCoreToOdt_Convert(mockGUI):
)
# Paragraph Styles
theDoc.theText = (
theDoc._theText = (
"### Scene\n\n"
"@pov: Jane\n"
"@char: John\n"
@@ -513,7 +513,7 @@ def testCoreToOdt_Convert(mockGUI):
assert getStyle("P8")._pAttr["margin-right"] == ["fo", "1.693cm"]
# Justified
theDoc.theText = (
theDoc._theText = (
"### Scene\n\n"
"Regular paragraph\n\n"
"with\nbreak\n\n"
@@ -536,7 +536,7 @@ def testCoreToOdt_Convert(mockGUI):
assert getStyle("P9")._pAttr["text-align"] == ["fo", "left"]
# Page Breaks
theDoc.theText = (
theDoc._theText = (
"## Chapter One\n\n"
"Text\n\n"
"## Chapter Two\n\n"
@@ -568,11 +568,11 @@ def testCoreToOdt_ConvertDirect(mockGUI):
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc.isNovel = True
theDoc._isNovel = True
# Justified
theDoc = ToOdt(theProject, isFlat=True)
theDoc.theTokens = [
theDoc._theTokens = [
(theDoc.T_TEXT, 1, "This is a paragraph", [], theDoc.A_JUSTIFY),
(theDoc.T_EMPTY, 1, "", None, theDoc.A_NONE),
]
@@ -593,7 +593,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
# Page Break After
theDoc = ToOdt(theProject, isFlat=True)
theDoc.theTokens = [
theDoc._theTokens = [
(theDoc.T_TEXT, 1, "This is a paragraph", [], theDoc.A_PBA),
(theDoc.T_EMPTY, 1, "", None, theDoc.A_NONE),
]
@@ -623,12 +623,12 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc.isNovel = True
theDoc._isNovel = True
assert theDoc.setLanguage(None) is False
assert theDoc.setLanguage("nb_NO") is True
theDoc.setColourHeaders(True)
theDoc.theText = (
theDoc._theText = (
"## Chapter One\n\n"
"Text\n\n"
"## Chapter Two\n\n"
@@ -660,9 +660,9 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=False)
theDoc.isNovel = True
theDoc._isNovel = True
theDoc.theText = (
theDoc._theText = (
"## Chapter One\n\n"
"Text\n\n"
"## Chapter Two\n\n"