Merge branch 'main' into xml_rw_class

This commit is contained in:
Veronica Berglyd Olsen
2022-10-21 10:24:00 +02:00
139 changed files with 7375 additions and 6828 deletions
+3
View File
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from novelwriter.core.doctools import DocMerger, DocSplitter
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -28,6 +29,8 @@ from novelwriter.core.toodt import ToOdt
from novelwriter.core.tomd import ToMarkdown
__all__ = [
"DocMerger",
"DocSplitter",
"countWords",
"NWDoc",
"NWProject",
+240
View File
@@ -0,0 +1,240 @@
"""
novelWriter Project Document Tools
====================================
A collection of tools to create and manipulate documents
File History:
Created: 2022-10-02 [2.0b1] DocMerger
Created: 2022-10-11 [2.0b1] DocSplitter
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from novelwriter.common import minmax
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class DocMerger:
def __init__(self, theProject):
self.theProject = theProject
self._error = ""
self._targetDoc = None
self._targetText = []
return
##
# Methods
##
def getError(self):
"""Return any collected errors.
"""
return self._error
def setTargetDoc(self, tHandle):
"""Set the target document for the merging. Calling this
function resets the class.
"""
self._targetDoc = tHandle
self._targetText = []
return
def newTargetDoc(self, srcHandle, docLabel):
"""Create a barnd new target document based on a source handle
and a new doc label. Calling this function resets the class.
"""
srcItem = self.theProject.tree[srcHandle]
if srcItem is None:
return None
newHandle = self.theProject.newFile(docLabel, srcItem.itemParent)
newItem = self.theProject.tree[newHandle]
newItem.setLayout(srcItem.itemLayout)
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
self._targetDoc = newHandle
self._targetText = []
return newHandle
def appendText(self, srcHandle, addComment, cmtPrefix):
"""Append text from an existing document to the text buffer.
"""
srcItem = self.theProject.tree[srcHandle]
if srcItem is None:
return False
inDoc = NWDoc(self.theProject, srcHandle)
docText = (inDoc.readDocument() or "").rstrip("\n")
if addComment:
docInfo = srcItem.describeMe()
docSt, _ = srcItem.getImportStatus(incIcon=False)
cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
docText = cmtLine + docText
self._targetText.append(docText)
return True
def writeTargetDoc(self):
"""Write the accumulated text into the designated target
document, appending any existing text.
"""
if self._targetDoc is None:
return False
outDoc = NWDoc(self.theProject, self._targetDoc)
docText = (outDoc.readDocument() or "").rstrip("\n")
if docText:
self._targetText.insert(0, docText)
status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
if not status:
self._error = outDoc.getError()
return status
# END Class DocMerger
class DocSplitter:
def __init__(self, theProject, sHandle):
self.theProject = theProject
self._error = ""
self._parHandle = None
self._srcHandle = None
self._srcItem = None
self._inFolder = False
self._rawData = []
srcItem = self.theProject.tree[sHandle]
if srcItem is not None and srcItem.isFileType():
self._srcHandle = sHandle
self._srcItem = srcItem
return
##
# Methods
##
def getError(self):
"""Return any collected errors.
"""
return self._error
def setParentItem(self, pHandle):
"""Set the item that will be the top level parent item for the
new documents.
"""
self._parHandle = pHandle
self._inFolder = False
return
def newParentFolder(self, pHandle, folderLabel):
"""Create a new folder that will be the top level parent item
for the new documents.
"""
if self._srcItem is None:
return None
newHandle = self.theProject.newFolder(folderLabel, pHandle)
newItem = self.theProject.tree[newHandle]
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
self._parHandle = newHandle
self._inFolder = True
return newHandle
def splitDocument(self, splitData, splitText):
"""Loop through the split data record and perform the split job.
"""
self._rawData = []
buffer = splitText.copy()
for lineNo, hLevel, hLabel in reversed(splitData):
chunk = buffer[lineNo:]
buffer = buffer[:lineNo]
self._rawData.insert(0, (chunk, hLevel, hLabel))
return True
def writeDocuments(self, docHierarchy):
"""An iterator that will write each document in the buffer, and
return its new handle, parent handle, and sibling handle.
"""
if self._srcHandle is None:
return
pHandle = self._parHandle
nHandle = self._parHandle if self._inFolder else self._srcHandle
hHandle = [self._parHandle, None, None, None, None]
pLevel = 0
for docText, hLevel, docLabel in self._rawData:
hLevel = minmax(hLevel, 1, 4)
if pLevel == 0:
pLevel = hLevel
if docHierarchy:
if hLevel == 1:
pHandle = self._parHandle
elif hLevel == 2:
pHandle = hHandle[1] or hHandle[0]
elif hLevel == 3:
pHandle = hHandle[2] or hHandle[1] or hHandle[0]
elif hLevel == 4:
pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
if hLevel < pLevel:
nHandle = hHandle[hLevel] or hHandle[0]
elif hLevel > pLevel:
nHandle = pHandle
dHandle = self.theProject.newFile(docLabel, pHandle)
hHandle[hLevel] = dHandle
outDoc = NWDoc(self.theProject, dHandle)
status = outDoc.writeDocument("\n".join(docText))
if not status:
self._error = outDoc.getError()
yield status, dHandle, nHandle
hHandle[hLevel] = dHandle
nHandle = dHandle
pLevel = hLevel
return
# END Class DocSplitter
+1 -1
View File
@@ -33,7 +33,7 @@ from novelwriter.common import isHandle, sha256sum
logger = logging.getLogger(__name__)
class NWDoc():
class NWDoc:
def __init__(self, theProject, theHandle):
+69 -74
View File
@@ -176,7 +176,7 @@ class NWIndex:
self._indexChange = round(time())
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
return True
@@ -202,7 +202,7 @@ class NWIndex:
logException()
return False
logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000)
logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
return True
@@ -243,20 +243,43 @@ class NWIndex:
if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle)
return False
if theItem.isInactive():
logger.debug("Not indexing inactive item '%s'", tHandle)
return False
logger.debug("Indexing item with handle '%s'", tHandle)
if theItem.isInactive():
self._scanInactive(theItem, theText)
else:
self._scanActive(tHandle, theItem, theText, itemTags)
# Scan the text content
# Update timestamps for index changes
nowTime = round(time())
self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime
return True
##
# Internal Indexer Helpers
##
def _scanActive(self, tHandle, theItem, theText, itemTags):
"""Scan an active document for meta data.
"""
nTitle = 0
findHeader = True
theLines = theText.splitlines()
for nLine, aLine in enumerate(theLines, start=1):
if len(aLine.strip()) == 0:
continue
if aLine.startswith("#"):
if findHeader:
hDepth, _ = self._splitHeading(aLine)
if hDepth != "H0":
theItem.setMainHeading(hDepth)
findHeader = False
isTitle = self._indexTitle(tHandle, aLine, nLine)
if isTitle and nLine > 0:
if nTitle > 0:
@@ -289,48 +312,49 @@ class NWIndex:
# Prune no longer used tags
for tTag, isActive in itemTags.items():
if not isActive:
logger.verbose("Deleting removed tag '%s'", tTag)
logger.debug("Deleting removed tag '%s'", tTag)
del self._tagsIndex[tTag]
# Update timestamps for index changes
nowTime = round(time())
self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime
return
return True
def _scanInactive(self, theItem, theText):
"""Scan an inactive document for meta data.
"""
for aLine in theText.splitlines():
if aLine.startswith("#"):
hDepth, _ = self._splitHeading(aLine)
if hDepth != "H0":
theItem.setMainHeading(hDepth)
break
return
##
# Internal Indexer Helpers
##
def _splitHeading(self, aLine):
"""Split a heading into its header level and text value.
"""
if aLine.startswith("# "):
return "H1", aLine[2:].strip()
elif aLine.startswith("## "):
return "H2", aLine[3:].strip()
elif aLine.startswith("### "):
return "H3", aLine[4:].strip()
elif aLine.startswith("#### "):
return "H4", aLine[5:].strip()
elif aLine.startswith("#! "):
return "H1", aLine[3:].strip()
elif aLine.startswith("##! "):
return "H2", aLine[4:].strip()
return "H0", ""
def _indexTitle(self, tHandle, aLine, nTitle):
"""Save information about the title and its location in the
file to the index.
"""
if aLine.startswith("# "):
hDepth = "H1"
hText = aLine[2:].strip()
elif aLine.startswith("## "):
hDepth = "H2"
hText = aLine[3:].strip()
elif aLine.startswith("### "):
hDepth = "H3"
hText = aLine[4:].strip()
elif aLine.startswith("#### "):
hDepth = "H4"
hText = aLine[5:].strip()
elif aLine.startswith("#! "):
hDepth = "H1"
hText = aLine[3:].strip()
elif aLine.startswith("##! "):
hDepth = "H2"
hText = aLine[4:].strip()
else:
hDepth, hText = self._splitHeading(aLine)
if hDepth == "H0":
return False
sTitle = f"T{nTitle:06d}"
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
return True
def _indexWordCounts(self, tHandle, theText, nTitle):
@@ -493,18 +517,15 @@ class NWIndex:
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
]
def getHandleHeaderLevel(self, tHandle):
"""Get the header level of the first header of a handle.
"""
return self._itemIndex.mainItemHeader(tHandle)
def getTableOfContents(self, maxDepth, skipExcl=True):
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
"""Generate a table of contents up to a maximum depth.
"""
tOrder = []
tData = {}
pKey = None
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
rootHandle=rootHandle, skipExcl=skipExcl
):
tKey = f"{tHandle}:{sTitle}"
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
if iLevel > maxDepth:
@@ -647,23 +668,17 @@ class TagsIndex:
def tagHandle(self, tagKey):
"""Get the handle of a given tag.
"""
if tagKey in self._tags:
return self._tags.get(tagKey).get("handle")
return None
return self._tags.get(tagKey, {}).get("handle", None)
def tagHeading(self, tagKey):
"""Get the heading of a given tag.
"""
if tagKey in self._tags:
return self._tags.get(tagKey).get("heading")
return nwHeaders.TT_NONE
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
def tagClass(self, tagKey):
"""Get the class of a given tag.
"""
if tagKey in self._tags:
return self._tags.get(tagKey).get("class")
return None
return self._tags.get(tagKey, {}).get("class", None)
##
# Pack/Unpack
@@ -755,13 +770,6 @@ class ItemIndex:
self._items[tHandle] = IndexItem(tHandle, tItem)
return
def mainItemHeader(self, tHandle):
"""Return the primary item header for an item.
"""
if tHandle in self._items:
return self._items[tHandle].level
return "H0"
def allItemTags(self, tHandle):
"""Get all tags set for headings of an item.
"""
@@ -794,7 +802,7 @@ class ItemIndex:
continue
if tItem.isNoteLayout():
continue
if skipExcl and not tItem.isExported:
if skipExcl and not tItem.isActive:
continue
tHandle = tItem.itemHandle
@@ -821,7 +829,6 @@ class ItemIndex:
"""
if tHandle in self._items:
tItem = self._items[tHandle]
tItem.updateLevel(hDepth)
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
return
@@ -897,7 +904,6 @@ class IndexItem:
def __init__(self, tHandle, tItem):
self._handle = tHandle
self._item = tItem
self._level = "H0"
self._headings = {}
self._index = 0
@@ -917,21 +923,10 @@ class IndexItem:
def item(self):
return self._item
@property
def level(self):
return self._level
##
# Setters
##
def updateLevel(self, level):
"""Set the level only if it has not already been set.
"""
if self._level == "H0":
self._level = level
return
def addHeading(self, tHeading):
"""Add a heading to the item. Also remove the placeholder entry
if it exists.
@@ -1011,7 +1006,7 @@ class IndexItem:
if hRefs:
refs[sTitle] = hRefs
data = {"level": self._level}
data = {}
data["headings"] = heads
if refs:
data["references"] = refs
@@ -1021,7 +1016,6 @@ class IndexItem:
def unpackData(self, data):
"""Unpack an item entry from the data.
"""
self._level = data.get("level", "H0")
references = data.get("references", {})
for sTitle, hData in data.get("headings", {}).items():
if not isTitleTag(sTitle):
@@ -1030,6 +1024,7 @@ class IndexItem:
tHeading.unpackData(hData)
tHeading.unpackReferences(references.get(sTitle, {}))
self.addHeading(tHeading)
return
# END Class IndexItem
+49 -29
View File
@@ -31,12 +31,12 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
)
from novelwriter.constants import nwLabels, trConst
from novelwriter.constants import nwHeaders, nwLabels, trConst
logger = logging.getLogger(__name__)
class NWItem():
class NWItem:
def __init__(self, theProject):
@@ -52,15 +52,16 @@ class NWItem():
self._layout = nwItemLayout.NO_LAYOUT
self._status = None
self._import = None
self._active = True
self._expanded = False
self._exported = True
# Document Meta Data
self._charCount = 0 # Current character count
self._wordCount = 0 # Current word count
self._paraCount = 0 # Current paragraph count
self._cursorPos = 0 # Last cursor position
self._initCount = 0 # Initial word count
self._heading = "H0" # The main heading
self._charCount = 0 # Current character count
self._wordCount = 0 # Current word count
self._paraCount = 0 # Current paragraph count
self._cursorPos = 0 # Last cursor position
self._initCount = 0 # Initial word count
return
@@ -114,13 +115,17 @@ class NWItem():
def itemImport(self):
return self._import
@property
def isActive(self):
return self._active
@property
def isExpanded(self):
return self._expanded
@property
def isExported(self):
return self._exported
def mainHeading(self):
return self._heading
@property
def charCount(self):
@@ -162,6 +167,7 @@ class NWItem():
metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE:
metaAttrib["mainHeading"] = str(self._heading)
metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount)
@@ -171,7 +177,7 @@ class NWItem():
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["exported"] = str(self._exported)
nameAttrib["active"] = str(self._active)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
self._subPack(xPack, "meta", attrib=metaAttrib)
@@ -202,6 +208,7 @@ class NWItem():
for xValue in xItem:
if xValue.tag == "meta":
self.setExpanded(xValue.attrib.get("expanded", False))
self.setMainHeading(xValue.attrib.get("mainHeading", "H0"))
self.setCharCount(xValue.attrib.get("charCount", 0))
self.setWordCount(xValue.attrib.get("wordCount", 0))
self.setParaCount(xValue.attrib.get("paraCount", 0))
@@ -210,7 +217,11 @@ class NWItem():
self.setName(xValue.text)
self.setStatus(xValue.attrib.get("status", None))
self.setImport(xValue.attrib.get("import", None))
self.setExported(xValue.attrib.get("exported", True))
self.setActive(xValue.attrib.get("active", True))
# ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
if "exported" in xValue.attrib:
self.setActive(xValue.attrib.get("exported", True))
# Legacy Format (1.3 and earlier)
elif xValue.tag == "status":
@@ -224,7 +235,7 @@ class NWItem():
elif xValue.tag == "expanded":
self.setExpanded(xValue.text)
elif xValue.tag == "exported":
self.setExported(xValue.text)
self.setActive(xValue.text)
elif xValue.tag == "charCount":
self.setCharCount(xValue.text)
elif xValue.tag == "wordCount":
@@ -269,7 +280,7 @@ class NWItem():
# Lookup Methods
##
def describeMe(self, hLevel=None):
def describeMe(self):
"""Return a string description of the item.
"""
descKey = "none"
@@ -279,12 +290,14 @@ class NWItem():
descKey = "folder"
elif self._type == nwItemType.FILE:
if self._layout == nwItemLayout.DOCUMENT:
if hLevel == "H1":
if self._heading == "H1":
descKey = "doc_h1"
elif hLevel == "H2":
elif self._heading == "H2":
descKey = "doc_h2"
elif hLevel == "H3":
elif self._heading == "H3":
descKey = "doc_h3"
elif self._heading == "H4":
descKey = "doc_h4"
else:
descKey = "document"
elif self._layout == nwItemLayout.NOTE:
@@ -292,16 +305,16 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def getImportStatus(self):
def getImportStatus(self, incIcon=True):
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
stIcon = self.theProject.statusItems.icon(self._status) if incIcon else None
else:
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
stIcon = self.theProject.importItems.icon(self._import) if incIcon else None
return stName, stIcon
##
@@ -487,6 +500,15 @@ class NWItem():
self._import = self.theProject.importItems.check(value)
return
def setActive(self, state):
"""Set the export flag.
"""
if isinstance(state, str):
self._active = (state == str(True))
else:
self._active = (state is True)
return
def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
@@ -496,19 +518,17 @@ class NWItem():
self._expanded = (state is True)
return
def setExported(self, state):
"""Set the export flag.
"""
if isinstance(state, str):
self._exported = (state == str(True))
else:
self._exported = (state is True)
return
##
# Set Document Meta Data
##
def setMainHeading(self, value):
"""Set the main heading level.
"""
if value in nwHeaders.H_LEVEL:
self._heading = value
return
def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
+7 -8
View File
@@ -40,30 +40,30 @@ VALID_MAP = {
"GuiWritingStats": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax",
},
"GuiDocSplit": {"spLevel"},
"GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
"GuiBuildNovel": {
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
"incSynopsis", "incComments", "incKeywords", "incBodyText",
"replaceTabs", "replaceUCode"
"replaceTabs", "replaceUCode", "rootFilter",
},
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
"GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
},
"GuiProjectDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
},
"GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol"},
}
class OptionState():
class OptionState:
def __init__(self, theProject):
self.theProject = theProject
@@ -188,8 +188,7 @@ class OptionState():
the default value.
"""
if group in self._theState:
if name in self._theState[group]:
return checkBool(self._theState[group].get(name, default), default)
return checkBool(self._theState[group].get(name, default), default)
return default
def getEnum(self, group, name, lookup, default):
+51 -36
View File
@@ -44,7 +44,7 @@ from novelwriter.core.document import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException
from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
checkString, checkBool, checkInt, checkStringNone, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt, minmax, simplified
)
from novelwriter.constants import trConst, nwFiles, nwLabels
@@ -52,7 +52,7 @@ from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__)
class NWProject():
class NWProject:
FILE_VERSION = "1.4" # The current project file format version
@@ -85,7 +85,6 @@ class NWProject():
self.projDict = None # The spell check dictionary
self.projSpell = None # The spell check language, if different than default
self.projLang = None # The project language, used for builds
self.projFile = None # The file name of the project main XML file
self.projFiles = [] # A list of all files in the content folder on load
# Project Meta
@@ -176,7 +175,7 @@ class NWProject():
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def writeNewFile(self, tHandle, hLevel, isDocument):
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
"""Write content to a new document after it is created. This
will not run if the file exists and is not empty.
"""
@@ -187,11 +186,11 @@ class NWProject():
return False
newDoc = NWDoc(self, tHandle)
if newDoc.readDocument().strip():
if (newDoc.readDocument() or "").strip():
return False
hshText = "#"*minmax(hLevel, 1, 4)
newText = f"{hshText} {tItem.itemName}\n\n"
newText = f"{hshText} {tItem.itemName}\n\n{addText}"
if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT)
else:
@@ -202,6 +201,23 @@ class NWProject():
return True
def removeItem(self, tHandle):
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
if self._projTree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self._projIndex.deleteHandle(tHandle)
del self._projTree[tHandle]
return True
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
@@ -243,7 +259,6 @@ class NWProject():
self.projDict = None
self.projSpell = None
self.projLang = None
self.projFile = nwFiles.PROJ_FILE
self.projFiles = []
self.projName = ""
self.bookTitle = ""
@@ -437,7 +452,7 @@ class NWProject():
legacyList = [] # Cleanup is done later
for projItem in os.listdir(self.projPath):
logger.verbose("Project contains: %s", projItem)
logger.debug("Project contains: %s", projItem)
if projItem.startswith("data_") and len(projItem) == 6:
legacyList.append(projItem)
@@ -457,7 +472,7 @@ class NWProject():
self.clearProject()
return False
else:
logger.verbose("Project is not locked")
logger.debug("Project is not locked")
# Open The Project XML File
# =========================
@@ -494,8 +509,8 @@ class NWProject():
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown"))
logger.verbose("XML root is '%s'", nwxRoot)
logger.verbose("File version is '%s'", fileVersion)
logger.debug("XML root is '%s'", nwxRoot)
logger.debug("File version is '%s'", fileVersion)
# Check File Type
# ===============
@@ -576,16 +591,16 @@ class NWProject():
if xItem.text is None:
continue
if xItem.tag == "name":
self.projName = checkString(simplified(xItem.text), "")
logger.verbose("Working Title: '%s'", self.projName)
self.projName = simplified(checkString(xItem.text, ""))
logger.info("Project Name: '%s'", self.projName)
elif xItem.tag == "title":
self.bookTitle = checkString(simplified(xItem.text), "")
logger.verbose("Title is '%s'", self.bookTitle)
self.bookTitle = simplified(checkString(xItem.text, ""))
logger.info("Project Title: '%s'", self.bookTitle)
elif xItem.tag == "author":
author = checkString(simplified(xItem.text), "")
author = simplified(checkString(xItem.text, ""))
if author:
self.bookAuthors.append(author)
logger.verbose("Author: '%s'", author)
logger.debug("Author: '%s'", author)
elif xItem.tag == "saveCount":
self.saveCount = checkInt(xItem.text, 0)
elif xItem.tag == "autoCount":
@@ -601,25 +616,25 @@ class NWProject():
if xItem.tag == "doBackup":
self.doBackup = checkBool(xItem.text, False)
elif xItem.tag == "language":
self.projLang = checkString(xItem.text, None, True)
self.projLang = checkStringNone(xItem.text, None)
elif xItem.tag == "spellCheck":
self.spellCheck = checkBool(xItem.text, False)
elif xItem.tag == "spellLang":
self.projSpell = checkString(xItem.text, None, True)
self.projSpell = checkStringNone(xItem.text, None)
elif xItem.tag == "lastEdited":
self.lastEdited = checkString(xItem.text, None, True)
self.lastEdited = checkStringNone(xItem.text, None)
elif xItem.tag == "lastViewed":
self.lastViewed = checkString(xItem.text, None, True)
self.lastViewed = checkStringNone(xItem.text, None)
elif xItem.tag == "lastNovel":
self.lastNovel = checkString(xItem.text, None, True)
self.lastNovel = checkStringNone(xItem.text, None)
elif xItem.tag == "lastOutline":
self.lastOutline = checkString(xItem.text, None, True)
self.lastOutline = checkStringNone(xItem.text, None)
elif xItem.tag == "lastWordCount":
self.lastWCount = checkInt(xItem.text, 0, False)
self.lastWCount = checkInt(xItem.text, 0)
elif xItem.tag == "novelWordCount":
self.lastNovelWC = checkInt(xItem.text, 0, False)
self.lastNovelWC = checkInt(xItem.text, 0)
elif xItem.tag == "notesWordCount":
self.lastNotesWC = checkInt(xItem.text, 0, False)
self.lastNotesWC = checkInt(xItem.text, 0)
elif xItem.tag == "status":
self.statusItems.unpackXML(xItem)
elif xItem.tag == "importance":
@@ -628,12 +643,12 @@ class NWProject():
for xEntry in xItem:
if xEntry.tag == "entry" and "key" in xEntry.attrib:
self.autoReplace[xEntry.attrib["key"]] = checkString(
xEntry.text, None, False
xEntry.text, "ERROR"
)
elif xItem.tag == "titleFormat":
titleFormat = self.titleFormat.copy()
for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
titleFormat[xEntry.tag] = checkString(xEntry.text, "")
self.setTitleFormat(titleFormat)
elif xChild.tag == "content":
@@ -663,7 +678,7 @@ class NWProject():
# Check the project tree consistency
for tItem in self._projTree:
tHandle = tItem.itemHandle
logger.verbose("Checking item '%s'", tHandle)
logger.debug("Checking item '%s'", tHandle)
if not self._projTree.updateItemData(tHandle):
logger.error("There was a problem item '%s', and it has been removed", tHandle)
del self._projTree[tHandle] # The file will be re-added as orphaned
@@ -757,9 +772,9 @@ class NWProject():
self._projTree.packXML(nwXML)
# Write the xml tree to file
tempFile = os.path.join(self.projPath, self.projFile+"~")
saveFile = os.path.join(self.projPath, self.projFile)
backFile = os.path.join(self.projPath, self.projFile[:-3]+"bak")
tempFile = os.path.join(self.projPath, nwFiles.PROJ_FILE+"~")
saveFile = os.path.join(self.projPath, nwFiles.PROJ_FILE)
backFile = os.path.join(self.projPath, nwFiles.PROJ_FILE[:-3]+"bak")
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
@@ -799,7 +814,7 @@ class NWProject():
return True
def closeProject(self, idleTime=0):
def closeProject(self, idleTime=0.0):
"""Close the current project and clear all meta data.
"""
logger.info("Closing project: %s", self.projPath)
@@ -1074,7 +1089,7 @@ class NWProject():
def setSpellLang(self, theLang):
"""Set the project-specific spell check language.
"""
theLang = checkString(theLang, None, True)
theLang = checkStringNone(theLang, None)
if self.projSpell != theLang:
self.projSpell = theLang
self.setProjectChanged(True)
@@ -1084,7 +1099,7 @@ class NWProject():
def setProjectLang(self, theLang):
"""Set the project-specific language.
"""
theLang = checkString(theLang, None, True)
theLang = checkStringNone(theLang, None)
if self.projLang != theLang:
self.projLang = theLang
self._loadProjectLocalisation()
@@ -1168,7 +1183,7 @@ class NWProject():
information to the GUI statusbar.
"""
self.projChanged = bValue
self.mainGui.statusBar.doUpdateProjectStatus(bValue)
self.mainGui.mainStatus.doUpdateProjectStatus(bValue)
if bValue:
# If we've changed the project at all, this should be True
self.projAltered = True
+1 -1
View File
@@ -33,7 +33,7 @@ from novelwriter.error import logException
logger = logging.getLogger(__name__)
class NWSpellEnchant():
class NWSpellEnchant:
def __init__(self):
+1 -1
View File
@@ -37,7 +37,7 @@ from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
class NWStatus():
class NWStatus:
STATUS = 1
IMPORT = 2
+2 -2
View File
@@ -38,7 +38,7 @@ class ToHtml(Tokenizer):
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
super().__init__(theProject)
self._genMode = self.M_EXPORT
self._cssStyles = True
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
Tokenizer.doPreProcessing(self)
super().doPreProcessing()
self._theText = self._theText.translate(self._trMap)
return
+4 -10
View File
@@ -304,16 +304,10 @@ class Tokenizer(ABC):
if self._theItem is None:
return False
self._theText = ""
if theText is not None:
# If the text is set, just use that
self._theText = theText
else:
# Otherwise, load it from file
theDoc = NWDoc(self.theProject, theHandle)
theText = theDoc.readDocument()
if theText:
self._theText = theText
if theText is None:
theText = NWDoc(self.theProject, theHandle).readDocument() or ""
self._theText = theText
docSize = len(self._theText)
if docSize > nwConst.MAX_DOCSIZE:
+1 -1
View File
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
M_GH = 1 # GitHub Markdown
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
super().__init__(theProject)
self._genMode = self.M_STD
self._fullMD = []
+4 -4
View File
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
class ToOdt(Tokenizer):
def __init__(self, theProject, isFlat):
Tokenizer.__init__(self, theProject)
super().__init__(theProject)
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
# Auto-Style Classes
# =============================================================================================== #
class ODTParagraphStyle():
class ODTParagraphStyle:
"""Wrapper class for the paragraph style setting used by the
exporter. Only the used settings are exposed here to keep the class
minimal and fast.
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
# END Class ODTParagraphStyle
class ODTTextStyle():
class ODTTextStyle:
"""Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal
and fast.
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
X_SPAN_SING = 3
class XMLParagraph():
class XMLParagraph:
"""This is a helper class to manage the text content of a single
XML element using mixed content tags.
+13 -11
View File
@@ -38,7 +38,7 @@ from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
class NWTree():
class NWTree:
MAX_DEPTH = 1000 # Cap of tree traversing for loops
@@ -89,20 +89,20 @@ class NWTree():
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
return False
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
if nwItem.isRootType():
logger.verbose("Item '%s' is a root item", str(tHandle))
logger.debug("Item '%s' is a root item", str(tHandle))
self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle))
logger.debug("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
elif nwItem.itemClass == nwItemClass.TRASH:
if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle))
logger.debug("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
@@ -274,11 +274,13 @@ class NWTree():
return rootClasses
def iterRoots(self, itemClass):
"""Iterate over all items of a given class.
"""Iterate over all root items of a given class in order.
"""
for tHandle, nwItem in self._treeRoots.items():
if nwItem.itemClass == itemClass:
yield tHandle, nwItem
for tHandle in self._treeOrder:
nwItem = self.__getitem__(tHandle)
if nwItem is not None and nwItem.isRootType():
if itemClass is None or nwItem.itemClass == itemClass:
yield tHandle, nwItem
return
def isRoot(self, tHandle):
@@ -347,7 +349,7 @@ class NWTree():
# Save the temp list
self._treeOrder = tmpOrder
self._setTreeChanged(True)
logger.verbose("Project tree order updated")
logger.debug("Project tree order updated")
return
@@ -457,7 +459,7 @@ class NWTree():
"""Generate a unique item handle. In the event that the key
already exists, generate a new one.
"""
logger.verbose("Generating new handle")
logger.debug("Generating new handle")
handle = f"{random.getrandbits(52):013x}"
if handle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...")