Fix and optimise code (#904)

* Fix bug in early error reporting in main init
* Update docstrings and optimise code in GuiMain
* Update docstrings and optimise code in Config
* Update docstrings and optimise code in common module
* Update docstrings and optimise code in main project classes
* Update the OptionState class
* Update the spell checker class
* Update the file converter classes and extend tests
* Update the about, merge, split and item editor classes and extend tests
* Update item editor test
* Update about dialog tests
* Some minor test cleanup
* Fix typo and add clarification in contributing guide
This commit is contained in:
Veronica Berglyd Olsen
2021-10-14 20:35:42 +01:00
committed by GitHub
parent 2a06db0a2b
commit 1c45331b0a
31 changed files with 1042 additions and 666 deletions
+8 -11
View File
@@ -287,10 +287,8 @@ class NWIndex():
nLine = 0
nTitle = 0
theLines = theText.splitlines()
for aLine in theLines:
nLine += 1
nChar = len(aLine.strip())
if nChar == 0:
for nLine, aLine in enumerate(theLines, start=1):
if len(aLine.strip()) == 0:
continue
if aLine.startswith("#"):
@@ -363,7 +361,7 @@ class NWIndex():
else:
return False
sTitle = "T%06d" % nLine
sTitle = f"T{nLine:06d}"
self._fileIndex[tHandle][sTitle] = {
"level": hDepth,
"title": hText,
@@ -391,14 +389,13 @@ class NWIndex():
"pCount": 0,
"synopsis": "",
}
return
def _indexWordCounts(self, tHandle, theText, nTitle):
"""Count text stats and save the counts to the index.
"""
cC, wC, pC = countWords(theText)
sTitle = "T%06d" % nTitle
sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["cCount"] = cC
@@ -409,7 +406,7 @@ class NWIndex():
def _indexSynopsis(self, tHandle, theText, nTitle):
"""Save the synopsis to the index.
"""
sTitle = "T%06d" % nTitle
sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["synopsis"] = theText
@@ -428,7 +425,7 @@ class NWIndex():
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
return
sTitle = "T%06d" % nTitle
sTitle = f"T{nTitle:06d}"
if theBits[0] == nwKeyWords.TAG_KEY:
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
@@ -525,7 +522,7 @@ class NWIndex():
"""
for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in sorted(self._fileIndex[tHandle]):
tKey = "%s:%s" % (tHandle, sTitle)
tKey = f"{tHandle}:{sTitle}"
yield tKey, tHandle, sTitle, self._fileIndex[tHandle][sTitle]
def getNovelWordCount(self, skipExcluded=True):
@@ -559,7 +556,7 @@ class NWIndex():
return theCounts
for sTitle, sData in hRecord.items():
theCounts.append(("%s:%s" % (tHandle, sTitle), sData["wCount"]))
theCounts.append((f"{tHandle}:{sTitle}", sData["wCount"]))
return theCounts
+6 -15
View File
@@ -103,11 +103,8 @@ class NWItem():
logger.error("XML item entry does not have a handle")
return False
if "parent" in xItem.attrib:
self.setParent(xItem.attrib["parent"])
if "order" in xItem.attrib:
self.setOrder(xItem.attrib["order"])
self.setParent(xItem.attrib.get("parent", None))
self.setOrder(xItem.attrib.get("order", 0))
tmpStatus = ""
for xValue in xItem:
@@ -200,11 +197,8 @@ class NWItem():
def setHandle(self, theHandle):
"""Set the item handle, and ensure it is valid.
"""
if isinstance(theHandle, str):
if isHandle(theHandle):
self.itemHandle = theHandle
else:
self.itemHandle = None
if isHandle(theHandle):
self.itemHandle = theHandle
else:
self.itemHandle = None
return
@@ -214,11 +208,8 @@ class NWItem():
"""
if theParent is None:
self.itemParent = None
elif isinstance(theParent, str):
if isHandle(theParent):
self.itemParent = theParent
else:
self.itemParent = None
elif isHandle(theParent):
self.itemParent = theParent
else:
self.itemParent = None
return
+70 -130
View File
@@ -30,88 +30,40 @@ import logging
import novelwriter
from novelwriter.constants import nwFiles
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
logger = logging.getLogger(__name__)
VALID_MAP = {
"GuiWritingStats": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
},
"GuiDocSplit": {"spLevel"},
"GuiBuildNovel": {
"winWidth", "winHeight", "boxWidth", "docWidth", "addNovel",
"addNotes", "ignoreFlag", "justifyText", "excludeBody", "textFont",
"textSize", "lineHeight", "noStyling", "incSynopsis", "incComments",
"incKeywords", "incBodyText", "replaceTabs", "replaceUCode"
},
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
"GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
},
"GuiProjectDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
},
"GuiWordList": {"winWidth", "winHeight"}
}
class OptionState():
def __init__(self, theProject):
self.theProject = theProject
self.theState = {}
self.validMap = {
"GuiWritingStats": {
"winWidth",
"winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"sortCol",
"sortOrder",
"incNovel",
"incNotes",
"hideZeros",
"hideNegative",
"groupByDay",
"showIdleTime",
"histMax",
},
"GuiDocSplit": {
"spLevel",
},
"GuiBuildNovel": {
"winWidth",
"winHeight",
"boxWidth",
"docWidth",
"addNovel",
"addNotes",
"ignoreFlag",
"justifyText",
"excludeBody",
"textFont",
"textSize",
"lineHeight",
"noStyling",
"incSynopsis",
"incComments",
"incKeywords",
"incBodyText",
"replaceTabs",
"replaceUCode",
},
"GuiOutline": {
"headerOrder",
"columnWidth",
"columnHidden",
},
"GuiProjectSettings": {
"winWidth",
"winHeight",
"replaceColW",
"statusColW",
"importColW",
},
"GuiProjectDetails": {
"winWidth",
"winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"widthCol4",
"wordsPerPage",
"countFrom",
"clearDouble",
},
"GuiWordList": {
"winWidth",
"winHeight",
}
}
self._theState = {}
return
##
@@ -139,11 +91,11 @@ class OptionState():
# Filter out unused variables
for aGroup in theState:
if aGroup in self.validMap:
self.theState[aGroup] = {}
if aGroup in VALID_MAP:
self._theState[aGroup] = {}
for anOpt in theState[aGroup]:
if anOpt in self.validMap[aGroup]:
self.theState[aGroup][anOpt] = theState[aGroup][anOpt]
if anOpt in VALID_MAP[aGroup]:
self._theState[aGroup][anOpt] = theState[aGroup][anOpt]
return True
@@ -158,7 +110,7 @@ class OptionState():
try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self.theState, outFile, indent=2)
json.dump(self._theState, outFile, indent=2)
except Exception:
logger.error("Failed to save GUI options file")
novelwriter.logException()
@@ -170,21 +122,21 @@ class OptionState():
# Setters
##
def setValue(self, setGroup, setName, setValue):
def setValue(self, group, name, value):
"""Saves a value, with a given group and name.
"""
if setGroup not in self.validMap:
logger.error("Unknown option group '%s'", setGroup)
if group not in VALID_MAP:
logger.error("Unknown option group '%s'", group)
return False
if setName not in self.validMap[setGroup]:
logger.error("Unknown option name '%s'", setName)
if name not in VALID_MAP[group]:
logger.error("Unknown option name '%s'", name)
return False
if setGroup not in self.theState:
self.theState[setGroup] = {}
if group not in self._theState:
self._theState[group] = {}
self.theState[setGroup][setName] = setValue
self._theState[group][name] = value
return True
@@ -192,79 +144,67 @@ class OptionState():
# Getters
##
def getValue(self, getGroup, getName, defaultValue):
def getValue(self, group, name, default):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
return self.theState[getGroup][getName]
return defaultValue
if group in self._theState:
return self._theState[group].get(name, default)
return default
def getString(self, getGroup, getName, defaultValue):
def getString(self, group, name, default):
"""Return the value as a string, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
return str(self.theState[getGroup][getName])
return defaultValue
if group in self._theState:
return checkString(self._theState[group].get(name, default), default)
return default
def getInt(self, getGroup, getName, defaultValue):
def getInt(self, group, name, default):
"""Return the value as an int, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return int(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return defaultValue
if group in self._theState:
return checkInt(self._theState[group].get(name, default), default)
return default
def getFloat(self, getGroup, getName, defaultValue):
def getFloat(self, group, name, default):
"""Return the value as a float, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return float(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return defaultValue
if group in self._theState:
return checkFloat(self._theState[group].get(name, default), default)
return default
def getBool(self, getGroup, getName, defaultValue):
def getBool(self, group, name, default):
"""Return the value as a bool, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
return bool(self.theState[getGroup][getName])
return defaultValue
if group in self._theState:
if name in self._theState[group]:
return checkBool(self._theState[group].get(name, default), default)
return default
##
# Validators
##
def validIntRange(self, theValue, intA, intB, intDefault):
def validIntRange(self, value, first, last, default):
"""Check that an int is in a given range. If it isn't, return
the default value.
"""
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
if isinstance(value, int):
if value >= first and value <= last:
return value
return default
def validIntTuple(self, theValue, theTuple, intDefault):
def validIntTuple(self, value, valid, default):
"""Check that an int is an element of a tuple. If it isn't,
return the default value.
"""
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
if isinstance(value, int):
if value in valid:
return value
return default
# END Class OptionState
+9 -8
View File
@@ -125,6 +125,7 @@ class NWProject():
if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None
newItem = NWItem(self)
newItem.setName(rootName)
newItem.setType(nwItemType.ROOT)
@@ -356,7 +357,7 @@ class NWProject():
return True
def openProject(self, fileName, overrideLock=False):
"""Open the project file provided, or if doesn't exist, assume
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
@@ -469,8 +470,8 @@ class NWProject():
# 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 notes. Introduced in version
# 1.5.
# novel documents and one for project notes. Introduced in
# version 1.5.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
self.theParent.makeAlert(self.tr(
@@ -973,7 +974,7 @@ class NWProject():
return False
self.bookAuthors = []
for bookAuthor in bookAuthors.split("\n"):
for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip()
if bookAuthor == "":
continue
@@ -1074,7 +1075,7 @@ class NWProject():
replaceMap = self.statusItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass == nwItemClass.NOVEL:
if nwItem.itemStatus in replaceMap.keys():
if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
@@ -1086,7 +1087,7 @@ class NWProject():
replaceMap = self.importItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass != nwItemClass.NOVEL:
if nwItem.itemStatus in replaceMap.keys():
if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
@@ -1104,7 +1105,7 @@ class NWProject():
"""
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
return True
def setProjectChanged(self, bValue):
@@ -1346,7 +1347,7 @@ class NWProject():
return
def _packProjectKeyValue(self, xParent, theName, theDict):
"""Pack the entries in the auto-replace dictionary.
"""Pack the entries of a dictionary into an xml element.
"""
xAutoRep = etree.SubElement(xParent, theName)
for aKey, aValue in theDict.items():
+40 -28
View File
@@ -35,16 +35,24 @@ class NWSpellEnchant():
def __init__(self):
self.mainConf = novelwriter.CONFIG
self.theDict = None
self.projDict = set()
self.projectDict = None
self.spellLanguage = None
self.theBroker = None
self._theDict = None
self._projDict = set()
self._projectDict = None
self._spellLanguage = None
self._theBroker = None
logger.debug("Enchant spell checking activated")
return
##
# Getters and Setters
##
def spellLanguage(self):
return self._spellLanguage
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't
@@ -52,49 +60,53 @@ class NWSpellEnchant():
"""
try:
import enchant
if self.theBroker is not None:
if self._theBroker is not None:
logger.debug("Deleting old pyenchant broker")
del self.theBroker
del self._theBroker
self.theBroker = enchant.Broker()
self.theDict = self.theBroker.request_dict(theLang)
self.spellLanguage = theLang
self._theBroker = enchant.Broker()
self._theDict = self._theBroker.request_dict(theLang)
self._spellLanguage = theLang
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
self.theDict = FakeEnchant()
self.spellLanguage = None
self._theDict = FakeEnchant()
self._spellLanguage = None
self._readProjectDictionary(projectDict)
for pWord in self.projDict:
self.theDict.add_to_session(pWord)
for pWord in self._projDict:
self._theDict.add_to_session(pWord)
return
##
# Methods
##
def checkWord(self, theWord):
"""Wrapper function for pyenchant.
"""
return self.theDict.check(theWord)
return self._theDict.check(theWord)
def suggestWords(self, theWord):
"""Wrapper function for pyenchant.
"""
return self.theDict.suggest(theWord)
return self._theDict.suggest(theWord)
def addWord(self, newWord):
"""Add a word to the project dictionary.
"""
self.theDict.add_to_session(newWord)
self._theDict.add_to_session(newWord)
if self.projectDict is not None and newWord not in self.projDict:
if self._projectDict is not None and newWord not in self._projDict:
newWord = newWord.strip()
try:
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
with open(self._projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self.projDict.add(newWord)
self._projDict.add(newWord)
except Exception:
logger.error("Failed to add word to project word list %s", str(self.projectDict))
logger.error("Failed to add word to project word list %s", str(self._projectDict))
novelwriter.logException()
return False
return True
@@ -119,8 +131,8 @@ class NWSpellEnchant():
dictionary.
"""
try:
spTag = self.theDict.tag
spName = self.theDict.provider.name
spTag = self._theDict.tag
spName = self._theDict.provider.name
except Exception:
logger.error("Failed to extract information about the dictionary")
novelwriter.logException()
@@ -137,8 +149,8 @@ class NWSpellEnchant():
"""Read the content of the project dictionary, and add it to the
lookup lists.
"""
self.projDict = set()
self.projectDict = projectDict
self._projDict = set()
self._projectDict = projectDict
if projectDict is None:
return False
@@ -151,9 +163,9 @@ class NWSpellEnchant():
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self.projDict:
self.projDict.add(theLine)
logger.debug("Project word list contains %d words", len(self.projDict))
if len(theLine) > 0 and theLine not in self._projDict:
self._projDict.add(theLine)
logger.debug("Project word list contains %d words", len(self._projDict))
except Exception:
logger.error("Failed to load project word list")
+59 -63
View File
@@ -109,7 +109,7 @@ class ToHtml(Tokenizer):
to theResult.
"""
if self.genMode == self.M_PREVIEW:
htmlTags = { # HTML4 + CSS2
htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "<b>",
self.FMT_B_E: "</b>",
self.FMT_I_B: "<i>",
@@ -118,7 +118,7 @@ class ToHtml(Tokenizer):
self.FMT_D_E: "</span>",
}
else:
htmlTags = { # HTML5
htmlTags = { # HTML5 (for export)
self.FMT_B_B: "<strong>",
self.FMT_B_E: "</strong>",
self.FMT_I_B: "<em>",
@@ -176,17 +176,18 @@ class ToHtml(Tokenizer):
aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L:
aStyle.append("margin-left: %dpx;" % self.mainConf.tabWidth)
aStyle.append(f"margin-left: {self.mainConf.tabWidth:d}px;")
if tStyle & self.A_IND_R:
aStyle.append("margin-right: %dpx;" % self.mainConf.tabWidth)
aStyle.append(f"margin-right: {self.mainConf.tabWidth:d}px;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
stVals = " ".join(aStyle)
hStyle = f" style='{stVals}'"
else:
hStyle = ""
if self.linkHeaders:
aNm = "<a name='T%06d'></a>" % tLine
aNm = f"<a name='T{tLine:06d}'></a>"
else:
aNm = ""
@@ -200,36 +201,36 @@ class ToHtml(Tokenizer):
parClass = ""
if len(thisPar) > 0:
tTemp = "<br/>".join(thisPar)
tmpResult.append("<p%s%s>%s</p>\n" % (parClass, parStyle, tTemp.rstrip()))
tmpResult.append(f"<p{parClass+parStyle}>{tTemp.rstrip()}</p>\n")
thisPar = []
parStyle = None
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1 class='title'%s>%s%s</h1>\n" % (hStyle, aNm, tHead))
tmpResult.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h3, hStyle, aNm, tHead, h3))
tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h4, hStyle, aNm, tHead, h4))
tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
elif tType == self.T_SEP:
tmpResult.append("<p class='sep'>%s</p>\n" % tText)
tmpResult.append(f"<p class='sep'>{tText}</p>\n")
elif tType == self.T_SKIP:
tmpResult.append("<p class='skip'>&nbsp;</p>\n")
@@ -249,7 +250,7 @@ class ToHtml(Tokenizer):
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tTemp = "<p%s>%s</p>\n" % (hStyle, self._formatKeywords(tText))
tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
tmpResult.append(tTemp)
self.theResult = "".join(tmpResult)
@@ -298,9 +299,9 @@ class ToHtml(Tokenizer):
"""Replace tabs with spaces in the html.
"""
htmlText = []
eightSpace = spaceChar*nSpaces
tabSpace = spaceChar*nSpaces
for aLine in self.fullHTML:
htmlText.append(aLine.replace("\t", eightSpace))
htmlText.append(aLine.replace("\t", tabSpace))
self.fullHTML = htmlText
return
@@ -315,75 +316,76 @@ class ToHtml(Tokenizer):
mScale = self.lineHeight/1.15
textAlign = "justify" if self.doJustify else "left"
theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % (
theStyles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
self.textFont, self.textSize
))
theStyles.append((
"p {"
"text-align: %s; line-height: %d%%; "
"margin-top: %.2fem; margin-bottom: %.2fem;"
"}"
) % (
"p {{"
"text-align: {0}; line-height: {1:d}%; "
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
"}}"
).format(
textAlign,
round(100 * self.lineHeight),
mScale * self.marginText[0],
mScale * self.marginText[1],
))
theStyles.append((
"h1 {"
"h1 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead1[0], mScale * self.marginHead1[1]
))
theStyles.append((
"h2 {"
"h2 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead2[0], mScale * self.marginHead2[1]
))
theStyles.append((
"h3 {"
"h3 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead3[0], mScale * self.marginHead3[1]
))
theStyles.append((
"h4 {"
"h4 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead4[0], mScale * self.marginHead4[1]
))
theStyles.append((
".title {"
".title {{"
"font-size: 2.5em; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginTitle[0], mScale * self.marginTitle[1]
))
theStyles.append((
".sep, .skip {"
".sep, .skip {{"
"text-align: center; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale, mScale
))
@@ -421,31 +423,25 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
"""
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span> " % nwLabels.KEY_NAME[theBits[0]]
retText += f"<span class='tags'>{nwLabels.KEY_NAME[theBits[0]]}:</span> "
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'>%s</a>" % (
theBits[1], theBits[1]
)
retText += f"<a name='tag_{theBits[1]}'>{theBits[1]}</a>"
else:
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
refTags.append(f"<a href='#{theBits[0][1:]}={tTag}'>{tTag}</a>")
retText += ", ".join(refTags)
else:
for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % (
tTag, tTag
))
refTags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
retText += ", ".join(refTags)
return retText
+5 -4
View File
@@ -267,13 +267,14 @@ class Tokenizer():
else:
textAlign = self.A_PBB | self.A_CENTRE
theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
locNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}"
self.theTokens = []
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, textAlign
))
if self.keepMarkdown:
self.theMarkdown.append("# %s\n\n" % theTitle)
self.theMarkdown.append(f"# {theTitle}\n\n")
return True
@@ -302,7 +303,7 @@ class Tokenizer():
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
self.theItem.itemName, f"{docSize/1.0e6:.2f}"
)
self.theText = "# %s\n\n%s\n\n" % (self.tr("ERROR"), 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
@@ -318,7 +319,7 @@ class Tokenizer():
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
repDict["<%s>" % aKey] = aVal
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)
+13 -11
View File
@@ -100,33 +100,33 @@ class ToMarkdown(Tokenizer):
# Process Text Type
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = " \n".join(thisPar)
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
tTemp = (" \n".join(thisPar)).rstrip(" ")
tmpResult.append(f"{tTemp}\n\n")
thisPar = []
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead)
tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead)
tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("### %s\n\n" % tHead)
tmpResult.append(f"### {tHead}\n\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("#### %s\n\n" % tHead)
tmpResult.append(f"#### {tHead}\n\n")
elif tType == self.T_SEP:
tmpResult.append("%s\n\n" % tText)
@@ -141,10 +141,12 @@ class ToMarkdown(Tokenizer):
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
locName = self._localLookup("Synopsis")
tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Comment"), tText))
locName = self._localLookup("Comment")
tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
@@ -189,7 +191,7 @@ class ToMarkdown(Tokenizer):
retText = ""
if theBits[0] in nwLabels.KEY_NAME:
retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]]
retText += f"**{nwLabels.KEY_NAME[theBits[0]]}:** "
if len(theBits) > 1:
retText += ", ".join(theBits[1:])
+40 -37
View File
@@ -45,18 +45,35 @@ XML_NS = {
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
MANI_NS = {
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
}
OFFICE_NS = {
"office": XML_NS["office"]
}
def _mkTag(nsName, tagName, nsMap=XML_NS):
"""Assemble namespace and tag name.
"""
theNS = nsMap.get(nsName, "")
if theNS:
return f"{{{theNS}}}{tagName}"
logger.warning("Missing xml namespace '%s'", nsName)
return tagName
# Mimetype and Version
X_MIME = "application/vnd.oasis.opendocument.text"
X_VERS = "1.2"
# Text Formatting Tags
TAG_BR = "{%s}line-break" % XML_NS["text"]
TAG_SPC = "{%s}s" % XML_NS["text"]
TAG_NSPC = "{%s}c" % XML_NS["text"]
TAG_TAB = "{%s}tab" % XML_NS["text"]
TAG_SPAN = "{%s}span" % XML_NS["text"]
TAG_STNM = "{%s}style-name" % XML_NS["text"]
TAG_BR = _mkTag("text", "line-break")
TAG_SPC = _mkTag("text", "s")
TAG_NSPC = _mkTag("text", "c")
TAG_TAB = _mkTag("text", "tab")
TAG_SPAN = _mkTag("text", "span")
TAG_STNM = _mkTag("text", "style-name")
# Formatting Codes
X_BLD = 0x01 # Bold format
@@ -313,7 +330,7 @@ class ToOdt(Tokenizer):
# Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{novelwriter.__version__}"
@@ -472,24 +489,22 @@ class ToOdt(Tokenizer):
def saveOpenDocText(self, savePath):
"""Save the data to an .odt file.
"""
mMap = {"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
mMani = "{%s}manifest" % mMap["manifest"]
mVers = "{%s}version" % mMap["manifest"]
mPath = "{%s}full-path" % mMap["manifest"]
mType = "{%s}media-type" % mMap["manifest"]
mFile = "{%s}file-entry" % mMap["manifest"]
mMani = _mkTag("manifest", "manifest", nsMap=MANI_NS)
mVers = _mkTag("manifest", "version", nsMap=MANI_NS)
mPath = _mkTag("manifest", "full-path", nsMap=MANI_NS)
mType = _mkTag("manifest", "media-type", nsMap=MANI_NS)
mFile = _mkTag("manifest", "file-entry", nsMap=MANI_NS)
xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=mMap)
xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=MANI_NS)
etree.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
sMap = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
oRoot = "{%s}document-settings" % sMap["office"]
oSett = "{%s}settings" % sMap["office"]
xSett = etree.Element(oRoot, nsmap=sMap)
oRoot = _mkTag("office", "document-settings", nsMap=OFFICE_NS)
oSett = _mkTag("office", "settings", nsMap=OFFICE_NS)
xSett = etree.Element(oRoot, nsmap=OFFICE_NS)
etree.SubElement(xSett, oSett)
with ZipFile(savePath, mode="w") as outFile:
@@ -520,16 +535,16 @@ class ToOdt(Tokenizer):
"""Apply formatting to synopsis lines.
"""
sSynop = self._localLookup("Synopsis")
rTxt = "**%s:** %s" % (sSynop, tText)
rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText))
rTxt = "**{0}:** {1}".format(sSynop, tText)
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(tText))
return rTxt, rFmt
def _formatComments(self, tText):
"""Apply formatting to comments.
"""
sComm = self._localLookup("Comment")
rTxt = "**%s:** %s" % (sComm, tText)
rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText))
rTxt = "**{0}:** {1}".format(sComm, tText)
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(tText))
return rTxt, rFmt
def _formatKeywords(self, tText):
@@ -543,8 +558,8 @@ class ToOdt(Tokenizer):
rFmt = ""
if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]]
rTxt += "**%s:** " % tText
rFmt += "_B%s b_ " % (" "*len(tText))
rTxt += "**{0}:** ".format(tText)
rFmt += "_B{0} b_ ".format(" "*len(tText))
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += theBits[1]
@@ -1468,16 +1483,4 @@ class XMLParagraph():
return
# =============================================================================================== #
# Local Functions
# =============================================================================================== #
def _mkTag(nsName, tagName):
"""Assemble namespace and tag name.
"""
theNS = XML_NS.get(nsName, "")
if theNS:
return "{%s}%s" % (theNS, tagName)
logger.warning("Missing xml namespace '%s'", nsName)
return tagName
# END Class XMLParagraph
+5 -5
View File
@@ -158,7 +158,7 @@ class NWTree():
continue
tFile = tHandle+".nwd"
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
tocLine = "%-25s %-9s %-8s %s" % (
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
os.path.join("content", tFile),
tItem.itemClass.name,
tItem.itemLayout.name,
@@ -175,7 +175,7 @@ class NWTree():
outFile.write("Table of Contents\n")
outFile.write("=================\n")
outFile.write("\n")
outFile.write("%-25s %-9s %-8s %s\n" % (
outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
"File Name", "Class", "Layout", "Document Label"
))
outFile.write("-"*max(tocLen, 62) + "\n")
@@ -285,12 +285,12 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
for i in range(nwConst.MAX_DEPTH + 1):
for _ in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None:
return tTree
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
tItem = self.__getitem__(tHandle)
if tItem is None:
return tTree
else:
@@ -341,7 +341,7 @@ class NWTree():
if tItem is None:
return False
if tItem.itemType != nwItemType.FILE:
logger.error("Item %s is not a file", tHandle)
logger.error("Item '%s' is not a file", tHandle)
return False
if not isinstance(itemLayout, nwItemLayout):
return False