Merge branch 'main' into merge_patches

This commit is contained in:
Veronica Berglyd Olsen
2022-08-17 23:21:10 +02:00
178 changed files with 17390 additions and 9704 deletions
+1 -2
View File
@@ -20,7 +20,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from novelwriter.core.document import NWDoc
from novelwriter.core.index import NWIndex, countWords
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
from novelwriter.core.spellcheck import NWSpellEnchant
from novelwriter.core.tohtml import ToHtml
@@ -30,7 +30,6 @@ from novelwriter.core.tomd import ToMarkdown
__all__ = [
"countWords",
"NWDoc",
"NWIndex",
"NWProject",
"NWSpellEnchant",
"ToHtml",
+1 -1
View File
@@ -52,7 +52,7 @@ class NWDoc():
self._docHandle = theHandle
if self._docHandle is not None:
self._theItem = self.theProject.projTree[theHandle]
self._theItem = self.theProject.tree[theHandle]
return
+749 -367
View File
File diff suppressed because it is too large Load Diff
+203 -78
View File
@@ -29,9 +29,9 @@ from lxml import etree
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
)
from novelwriter.constants import nwLabels, nwLists, trConst
from novelwriter.constants import nwLabels, trConst
logger = logging.getLogger(__name__)
@@ -45,11 +45,13 @@ class NWItem():
self._name = ""
self._handle = None
self._parent = None
self._root = None
self._order = 0
self._type = nwItemType.NO_TYPE
self._class = nwItemClass.NO_CLASS
self._layout = nwItemLayout.NO_LAYOUT
self._status = None
self._import = None
self._expanded = False
self._exported = True
@@ -84,6 +86,10 @@ class NWItem():
def itemParent(self):
return self._parent
@property
def itemRoot(self):
return self._root
@property
def itemOrder(self):
return self._order
@@ -104,6 +110,10 @@ class NWItem():
def itemStatus(self):
return self._status
@property
def itemImport(self):
return self._import
@property
def isExpanded(self):
return self._expanded
@@ -139,24 +149,33 @@ class NWItem():
def packXML(self, xParent):
"""Pack all the data in the class instance into an XML object.
"""
xPack = etree.SubElement(xParent, "item", attrib={
"handle": str(self._handle),
"order": str(self._order),
"parent": str(self._parent),
})
self._subPack(xPack, "name", text=str(self._name))
self._subPack(xPack, "type", text=str(self._type.name))
self._subPack(xPack, "class", text=str(self._class.name))
self._subPack(xPack, "status", text=str(self._status))
itemAttrib = {}
itemAttrib["handle"] = str(self._handle)
itemAttrib["parent"] = str(self._parent)
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order)
itemAttrib["type"] = str(self._type.name)
itemAttrib["class"] = str(self._class.name)
if self._type == nwItemType.FILE:
self._subPack(xPack, "exported", text=str(self._exported))
self._subPack(xPack, "layout", text=str(self._layout.name))
self._subPack(xPack, "charCount", text=str(self._charCount), none=False)
self._subPack(xPack, "wordCount", text=str(self._wordCount), none=False)
self._subPack(xPack, "paraCount", text=str(self._paraCount), none=False)
self._subPack(xPack, "cursorPos", text=str(self._cursorPos), none=False)
else:
self._subPack(xPack, "expanded", text=str(self._expanded))
itemAttrib["layout"] = str(self._layout.name)
metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE:
metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount)
metaAttrib["cursorPos"] = str(self._cursorPos)
nameAttrib = {}
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["exported"] = str(self._exported)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
self._subPack(xPack, "meta", attrib=metaAttrib)
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
return
@@ -174,20 +193,34 @@ class NWItem():
return False
self.setParent(xItem.attrib.get("parent", None))
self.setRoot(xItem.attrib.get("root", None))
self.setOrder(xItem.attrib.get("order", 0))
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
tmpStatus = ""
for xValue in xItem:
if xValue.tag == "name":
if xValue.tag == "meta":
self.setExpanded(xValue.attrib.get("expanded", False))
self.setCharCount(xValue.attrib.get("charCount", 0))
self.setWordCount(xValue.attrib.get("wordCount", 0))
self.setParaCount(xValue.attrib.get("paraCount", 0))
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
elif xValue.tag == "name":
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))
# Legacy Format (1.3 and earlier)
elif xValue.tag == "status":
self.setImportStatus(xValue.text)
elif xValue.tag == "type":
self.setType(xValue.text)
elif xValue.tag == "class":
self.setClass(xValue.text)
elif xValue.tag == "layout":
self.setLayout(xValue.text)
elif xValue.tag == "status":
tmpStatus = xValue.text
elif xValue.tag == "expanded":
self.setExpanded(xValue.text)
elif xValue.tag == "exported":
@@ -206,8 +239,16 @@ class NWItem():
# version of novelWriter that doesn't know the tag
logger.error("Unknown tag '%s'", xValue.tag)
# Guarantees that <status> is parsed after <class>
self.setStatus(tmpStatus)
# Make some checks to ensure consistency
if self._type == nwItemType.ROOT:
self._root = self._handle # Root items are their own ancestor
self._parent = None # Root items cannot have a parent
if self._type != nwItemType.FILE:
self._charCount = 0 # Only set for files
self._wordCount = 0 # Only set for files
self._paraCount = 0 # Only set for files
self._cursorPos = 0 # Only set for files
return True
@@ -225,7 +266,7 @@ class NWItem():
return
##
# Methods
# Lookup Methods
##
def describeMe(self, hLevel=None):
@@ -251,142 +292,226 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def isNovelLike(self):
"""Returns true if the item is of a novel-like class.
"""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE)
def documentAllowed(self):
"""Returns true if the item is allowed to be of document layout.
"""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def isInactive(self):
"""Returns true if the item is in an inactive class.
"""
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def getImportStatus(self):
"""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)
else:
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon
##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport.
"""
if self.isNovelLike():
self.setStatus(value)
else:
self.setImport(value)
return
def setClassDefaults(self, itemClass):
"""Set the default values based on the item's class and the
project settings.
"""
if self._parent is not None:
# Only update for child items
self.setClass(itemClass)
if self._layout == nwItemLayout.NO_LAYOUT:
# If no layout is set, pick one
if self.isNovelLike():
self._layout = nwItemLayout.DOCUMENT
else:
self._layout = nwItemLayout.NOTE
elif not self.documentAllowed():
# Change layout to note if it is not in an allowed folder
self._layout = nwItemLayout.NOTE
if self._status is None:
self.setStatus("New") # This forces a default value lookup
if self._import is None:
self.setImport("New") # This forces a default value lookup
return
##
# Set Item Values
##
def setName(self, theName):
def setName(self, name):
"""Set the item name.
"""
if isinstance(theName, str):
self._name = theName.strip()
if isinstance(name, str):
self._name = simplified(name)
else:
self._name = ""
return
def setHandle(self, theHandle):
def setHandle(self, handle):
"""Set the item handle, and ensure it is valid.
"""
if isHandle(theHandle):
self._handle = theHandle
if isHandle(handle):
self._handle = handle
else:
self._handle = None
return
def setParent(self, theParent):
def setParent(self, handle):
"""Set the parent handle, and ensure it is valid.
"""
if theParent is None:
if handle is None:
self._parent = None
elif isHandle(theParent):
self._parent = theParent
elif isHandle(handle):
self._parent = handle
else:
self._parent = None
return
def setOrder(self, theOrder):
def setRoot(self, handle):
"""Set the root handle, and ensure it is valid.
"""
if handle is None:
self._root = None
elif isHandle(handle):
self._root = handle
else:
self._root = None
return
def setOrder(self, order):
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at
the moment.
"""
self._order = checkInt(theOrder, 0)
self._order = checkInt(order, 0)
return
def setType(self, theType):
def setType(self, value):
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
if isinstance(theType, nwItemType):
self._type = theType
elif isItemType(theType):
self._type = nwItemType[theType]
if isinstance(value, nwItemType):
self._type = value
elif isItemType(value):
self._type = nwItemType[value]
elif value == "TRASH":
self._type = nwItemType.ROOT
else:
logger.error("Unrecognised item type '%s'", theType)
logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE
return
def setClass(self, theClass):
def setClass(self, value):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
if isinstance(theClass, nwItemClass):
self._class = theClass
elif isItemClass(theClass):
self._class = nwItemClass[theClass]
if isinstance(value, nwItemClass):
self._class = value
elif isItemClass(value):
self._class = nwItemClass[value]
else:
logger.error("Unrecognised item class '%s'", theClass)
logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS
return
def setLayout(self, theLayout):
def setLayout(self, value):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
if isinstance(theLayout, nwItemLayout):
self._layout = theLayout
elif isItemLayout(theLayout):
self._layout = nwItemLayout[theLayout]
elif theLayout in nwLists.DEP_LAYOUT:
if isinstance(value, nwItemLayout):
self._layout = value
elif isItemLayout(value):
self._layout = nwItemLayout[value]
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
self._layout = nwItemLayout.DOCUMENT
else:
logger.error("Unrecognised item layout '%s'", theLayout)
logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, theStatus):
def setStatus(self, value):
"""Set the item status by looking it up in the valid status
items of the current project.
"""
if self._class in nwLists.CLS_NOVEL:
self._status = self.theProject.statusItems.checkEntry(theStatus)
else:
self._status = self.theProject.importItems.checkEntry(theStatus)
self._status = self.theProject.statusItems.check(value)
return
def setExpanded(self, expState):
def setImport(self, value):
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self.theProject.importItems.check(value)
return
def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
if isinstance(expState, str):
self._expanded = (expState == str(True))
if isinstance(state, str):
self._expanded = (state == str(True))
else:
self._expanded = (expState is True)
self._expanded = (state is True)
return
def setExported(self, expState):
def setExported(self, state):
"""Set the export flag.
"""
if isinstance(expState, str):
self._exported = (expState == str(True))
if isinstance(state, str):
self._exported = (state == str(True))
else:
self._exported = (expState is True)
self._exported = (state is True)
return
##
# Set Document Meta Data
##
def setCharCount(self, theCount):
def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
self._charCount = max(0, checkInt(theCount, 0))
self._charCount = max(0, checkInt(count, 0))
return
def setWordCount(self, theCount):
def setWordCount(self, count):
"""Set the word count, and ensure that it is an integer.
"""
self._wordCount = max(0, checkInt(theCount, 0))
self._wordCount = max(0, checkInt(count, 0))
return
def setParaCount(self, theCount):
def setParaCount(self, count):
"""Set the paragraph count, and ensure that it is an integer.
"""
self._paraCount = max(0, checkInt(theCount, 0))
self._paraCount = max(0, checkInt(count, 0))
return
def setCursorPos(self, thePosition):
def setCursorPos(self, position):
"""Set the cursor position, and ensure that it is an integer.
"""
self._cursorPos = max(0, checkInt(thePosition, 0))
self._cursorPos = max(0, checkInt(position, 0))
return
def saveInitialCount(self):
+20 -2
View File
@@ -28,6 +28,8 @@ import os
import json
import logging
from enum import Enum
from novelwriter.error import logException
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
from novelwriter.constants import nwFiles
@@ -56,7 +58,8 @@ VALID_MAP = {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
},
"GuiWordList": {"winWidth", "winHeight"}
"GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol"},
}
@@ -137,7 +140,10 @@ class OptionState():
if group not in self._theState:
self._theState[group] = {}
self._theState[group][name] = value
if isinstance(value, Enum):
self._theState[group][name] = value.name
else:
self._theState[group][name] = value
return True
@@ -186,4 +192,16 @@ class OptionState():
return checkBool(self._theState[group].get(name, default), default)
return default
def getEnum(self, group, name, lookup, default):
"""Return the value mapped to an enum. Otherwise return the
default value
"""
if issubclass(lookup, Enum):
if group in self._theState:
if name in self._theState[group]:
value = self._theState[group][name]
if value in lookup.__members__:
return lookup[value]
return default
# END Class OptionState
File diff suppressed because it is too large Load Diff
+207 -102
View File
@@ -4,7 +4,8 @@ novelWriter Project Item Status Class
Data class for the status/importance settings of a project item
File History:
Created: 2019-05-19 [0.1.3]
Created: 2019-05-19 [0.1.3]
Rewritten: 2022-04-05 [1.7a0]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
@@ -23,165 +24,269 @@ 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 random
import logging
import novelwriter
from lxml import etree
from novelwriter.common import checkInt
from PyQt5.QtGui import QIcon, QPixmap, QColor
from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
class NWStatus():
def __init__(self):
STATUS = 1
IMPORT = 2
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
def __init__(self, type):
self._type = type
self._store = {}
self._reverse = {}
self._default = None
self._iconSize = novelwriter.CONFIG.pxInt(32)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(100, 100, 100))
self._defaultIcon = QIcon(pixmap)
if self._type == self.STATUS:
self._prefix = "s"
elif self._type == self.IMPORT:
self._prefix = "i"
else:
raise Exception("This is a bug!")
return
def addEntry(self, theLabel, theColours):
"""Add a status entry to the status object, but ensure it isn't
a duplicate.
def write(self, key, name, cols, count=None):
"""Add or update a status entry. If the key is invalid, a new
key is generated.
"""
theLabel = theLabel.strip()
if self.lookupEntry(theLabel) is None:
self._theLabels.append(theLabel)
self._theColours.append(theColours)
self._theCounts.append(0)
self._theMap[theLabel] = self._theLength
self._theLength += 1
if not self._isKey(key):
key = self._newKey()
if not isinstance(cols, tuple):
cols = (100, 100, 100)
if len(cols) != 3:
cols = (100, 100, 100)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(*cols))
name = simplified(name)
if count is None:
count = self._store[key]["count"] if key in self._store else 0
self._store[key] = {
"name": name,
"icon": QIcon(pixmap),
"cols": cols,
"count": count,
}
self._reverse[name] = key
if self._default is None:
self._default = key
return key
def remove(self, key):
"""Remove an entry in the list, but not if the count is larger
than 0.
"""
if key not in self._store:
return False
if self._store[key]["count"] > 0:
return False
del self._reverse[self._store[key]["name"]]
del self._store[key]
keys = list(self._store.keys())
if key == self._default:
if len(keys) > 0:
self._default = keys[0]
else:
self._default = None
return True
def lookupEntry(self, theLabel):
"""Look up a status entry in the object lists, and return it if
it exists.
def check(self, value):
"""Check the key against the stored status names.
"""
if theLabel is None:
return None
theLabel = theLabel.strip()
if theLabel in self._theMap.keys():
return self._theMap[theLabel]
return None
if self._isKey(value) and value in self._store:
return value
elif value in self._reverse:
return self._reverse[value]
elif self._default is not None:
return self._default
else:
return ""
def checkEntry(self, theStatus):
"""Check if a status value is valid, and returns the safe
reference to be used internally.
def name(self, key):
"""Return the name associated with a given key.
"""
if isinstance(theStatus, str):
theStatus = theStatus.strip()
if self.lookupEntry(theStatus) is not None:
return theStatus
theStatus = checkInt(theStatus, 0, False)
if theStatus >= 0 and theStatus < self._theLength:
return self._theLabels[theStatus]
return self._theLabels[0]
if key in self._store:
return self._store[key]["name"]
elif self._default is not None:
return self._store[self._default]["name"]
else:
return ""
def setNewEntries(self, newList):
"""Update the list of entries after they have been modified by
the GUI tool.
def cols(self, key):
"""Return the colours associated with a given key.
"""
replaceMap = {}
if key in self._store:
return self._store[key]["cols"]
elif self._default is not None:
return self._store[self._default]["cols"]
else:
return (100, 100, 100)
if newList is not None:
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
def count(self, key):
"""Return the count associated with a given key.
"""
if key in self._store:
return self._store[key]["count"]
elif self._default is not None:
return self._store[self._default]["count"]
else:
return 0
for nName, nR, nG, nB, oName in newList:
self.addEntry(nName, (nR, nG, nB))
if nName != oName and oName is not None:
replaceMap[oName] = nName
def icon(self, key):
"""Return the icon associated with a given key.
"""
if key in self._store:
return self._store[key]["icon"]
elif self._default is not None:
return self._store[self._default]["icon"]
else:
return self._defaultIcon
return replaceMap
def reorder(self, order):
"""Reorder the items according to list.
"""
if len(order) != len(self._store):
logger.error("Length mismatch between new and old order")
return False
if order == list(self._store.keys()):
return False
store = {}
for key in order:
if key in self._store:
store[key] = self._store[key]
else:
logger.error("Unknown key '%s' in order", key)
return False
self._store = store
return True
def resetCounts(self):
"""Clear the counts of references to the status entries.
"""
self._theCounts = [0]*self._theLength
for key in self._store:
self._store[key]["count"] = 0
return
def countEntry(self, theLabel):
"""Increment the counter for a given label. This should be used
together with resetCounts in a loop over project items.
def increment(self, key):
"""Increment the counter for a given entry.
"""
theIndex = self.lookupEntry(theLabel)
if theIndex is not None:
self._theCounts[theIndex] += 1
if key in self._store:
self._store[key]["count"] += 1
return
def packXML(self, xParent):
"""Pack the status entries into an XML object for saving to the
main project file.
"""
for n in range(self._theLength):
for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={
"blue": str(self._theColours[n][2]),
"green": str(self._theColours[n][1]),
"red": str(self._theColours[n][0]),
"key": key,
"count": str(data["count"]),
"red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
})
xSub.text = self._theLabels[n]
xSub.text = data["name"]
return True
def unpackXML(self, xParent):
"""Unpack an XML tree and set the class values.
"""
theLabels = []
theColours = []
self._store = {}
self._reverse = {}
self._default = None
for xChild in xParent:
theLabels.append(xChild.text)
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
theColours.append((cR, cG, cB))
key = xChild.attrib.get("key", None)
name = xChild.text.strip()
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
self.write(key, name, (red, green, blue), count)
if len(theLabels) > 0:
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
return True
for n in range(len(theLabels)):
self.addEntry(theLabels[n], theColours[n])
##
# Internal Functions
##
def _newKey(self):
"""Generate a new key for a status flag. This method is
recursive, but should only fail if there is an issue with the
random number generator or the user has added a lot of status
flags. The Python recursion limit is given the job to handle
the extreme case and will cause an app crash.
"""
key = f"{self._prefix}{random.getrandbits(24):06x}"
if key in self._store:
key = self._newKey()
return key
def _isKey(self, value):
"""Check if a value is a key or not.
"""
if not isinstance(value, str):
return False
if len(value) != 7:
return False
if value[0] != self._prefix:
return False
for c in value[1:]:
if c not in "0123456789abcdef":
return False
return True
##
# Iterator Bits
##
def __getitem__(self, n):
"""Return an entry by its index.
"""
if n >= 0 and n < self._theLength:
return self._theLabels[n], self._theColours[n], self._theCounts[n]
return None, None, None
def __len__(self):
return len(self._store)
def __getitem__(self, key):
return self._store[key]
def __iter__(self):
"""Initialise the iterator.
"""
self._theIndex = 0
return self
return iter(self._store)
def __next__(self):
"""Return the next entry for the iterator.
"""
if self._theIndex < self._theLength:
theLabel, theColour, theCount = self.__getitem__(self._theIndex)
self._theIndex += 1
return theLabel, theColour, theCount
else:
raise StopIteration
def keys(self):
return self._store.keys()
def items(self):
return self._store.items()
def values(self):
return self._store.values()
# END Class NWStatus
+1 -1
View File
@@ -451,7 +451,7 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
"""
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits:
return ""
+10 -6
View File
@@ -1,7 +1,7 @@
"""
novelWriter Text Tokenizer
============================
Splits a piece of novelWriter markdown text into its elements
Split novelWriter plain text into its elements
File History:
Created: 2019-05-05 [0.0.1]
@@ -27,6 +27,7 @@ import re
import logging
import novelwriter
from abc import ABC, abstractmethod
from operator import itemgetter
from functools import partial
@@ -40,7 +41,7 @@ from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class Tokenizer():
class Tokenizer(ABC):
# In-Text Format
FMT_B_B = 1 # Begin bold
@@ -81,7 +82,6 @@ class Tokenizer():
def __init__(self, theProject):
self.theProject = theProject
self.theParent = theProject.theParent
self.mainConf = novelwriter.CONFIG
# Data Variables
@@ -267,10 +267,14 @@ class Tokenizer():
# Class Methods
##
@abstractmethod
def doConvert(self):
raise NotImplementedError
def addRootHeading(self, theHandle):
"""Add a heading at the start of a new root folder.
"""
if not self.theProject.projTree.checkType(theHandle, nwItemType.ROOT):
if not self.theProject.tree.checkType(theHandle, nwItemType.ROOT):
return False
if self._isFirst:
@@ -279,7 +283,7 @@ class Tokenizer():
else:
textAlign = self.A_PBB | self.A_CENTRE
theItem = self.theProject.projTree[theHandle]
theItem = self.theProject.tree[theHandle]
locNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}"
self._theTokens = []
@@ -296,7 +300,7 @@ class Tokenizer():
not set, load it from the file.
"""
self._theHandle = theHandle
self._theItem = self.theProject.projTree[theHandle]
self._theItem = self.theProject.tree[theHandle]
if self._theItem is None:
return False
+1 -1
View File
@@ -193,7 +193,7 @@ class ToMarkdown(Tokenizer):
def _formatKeywords(self, tText, tStyle):
"""Apply Markdown formatting to keywords.
"""
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits:
return ""
+2 -2
View File
@@ -330,7 +330,7 @@ class ToOdt(Tokenizer):
# Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
xMeta.text = datetime.now().isoformat(sep="T", timespec="seconds")
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{novelwriter.__version__}"
@@ -550,7 +550,7 @@ class ToOdt(Tokenizer):
def _formatKeywords(self, tText):
"""Apply formatting to keywords.
"""
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits:
return ""
+109 -137
View File
@@ -24,16 +24,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import random
import logging
from time import time
from lxml import etree
from hashlib import sha256
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkHandle
from novelwriter.constants import nwConst, nwFiles
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
@@ -41,21 +40,20 @@ logger = logging.getLogger(__name__)
class NWTree():
MAX_DEPTH = 1000 # Cap of tree traversing for loops
def __init__(self, theProject):
self.theProject = theProject
self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = [] # The root items of the tree
self._treeRoots = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed
self._handleSeed = None # Used for generating handles for testing
self._handleCount = 0 # A counter that is added to the handle generator
return
##
@@ -67,7 +65,7 @@ class NWTree():
"""
self._projTree = {}
self._treeOrder = []
self._treeRoots = []
self._treeRoots = {}
self._trashRoot = None
self._archRoot = None
self._theIndex = 0
@@ -98,18 +96,17 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item '%s' is a root item", str(tHandle))
self._treeRoots.append(tHandle)
self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
if nwItem.itemType == nwItemType.TRASH:
if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
return False
elif nwItem.itemClass == nwItemClass.TRASH:
if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
return False
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
@@ -207,9 +204,30 @@ class NWTree():
return novelWords, noteWords
##
# Tree Structure Methods
# Tree Item Methods
##
def updateItemData(self, tHandle):
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
iItem = tItem
for _ in range(self.MAX_DEPTH):
if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass)
return True
else:
iItem = self.__getitem__(iItem.itemParent)
if iItem is None:
return False
else:
raise RecursionError("Critical internal error")
def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type.
"""
@@ -218,71 +236,6 @@ class NWTree():
return False
return tItem.itemType == itemType
def trashRoot(self):
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
if self._trashRoot:
return self._trashRoot
return None
def isTrashRoot(self, tHandle):
"""Check if a handle is the trash folder.
"""
if self._trashRoot is None:
return False
return tHandle == self._trashRoot
def archiveRoot(self):
"""Returns the handle of the archive folder, or None if there
isn't one.
"""
if self._archRoot:
return self._archRoot
return None
def findRoot(self, theClass):
"""Find the root item for a given class.
Note: This returns the first item for class CUSTOM.
"""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return tItem.itemHandle
return None
def checkRootUnique(self, theClass):
"""Checks if there already is a root entry of class 'theClass'
in the root of the project tree. CUSTOM class is skipped as it
is not required to be unique.
"""
if theClass == nwItemClass.CUSTOM:
return True
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return False
return True
def getRootItem(self, tHandle):
"""Iterate upwards in the tree until we find the item with
parent None, the root item. We do this with a for loop with a
maximum depth to make infinite loops impossible.
"""
tItem = self.__getitem__(tHandle)
if tItem is not None:
for i in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None:
return tItem
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
return None
def getItemPath(self, tHandle):
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles.
@@ -293,7 +246,7 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
for _ in range(nwConst.MAX_DEPTH + 1):
for _ in range(self.MAX_DEPTH):
if tItem.itemParent is None:
return tTree
else:
@@ -303,8 +256,72 @@ class NWTree():
return tTree
else:
tTree.append(tHandle)
else:
raise RecursionError("Critical internal error")
return tTree
##
# Tree Root Methods
##
def rootClasses(self):
"""Return a set of all root classes in use by the project.
"""
rootClasses = set()
for nwItem in self._treeRoots.values():
rootClasses.add(nwItem.itemClass)
return rootClasses
def iterRoots(self, itemClass):
"""Iterate over all items of a given class.
"""
for tHandle, nwItem in self._treeRoots.items():
if nwItem.itemClass == itemClass:
yield tHandle, nwItem
return
def isRoot(self, tHandle):
"""Check if a handle is a root item.
"""
return tHandle in self._treeRoots
def isTrash(self, tHandle):
"""Check if an item is in or is the trash folder.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return True
if tItem.itemClass == nwItemClass.TRASH:
return True
if self._trashRoot is not None:
if tHandle == self._trashRoot:
return True
elif tItem.itemParent == self._trashRoot:
return True
elif tItem.itemRoot == self._trashRoot:
return True
return False
def trashRoot(self):
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
if self._trashRoot:
return self._trashRoot
return None
def findRoot(self, theClass):
"""Find the first root item for a given class.
"""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return tItem.itemHandle
return None
##
# Setters
##
@@ -334,14 +351,6 @@ class NWTree():
return
def setSeed(self, theSeed):
"""Used for debugging!
Sets a seed for generating handles so that they always come out
in a predictable order.
"""
self._handleSeed = theSeed
return
def setFileItemLayout(self, tHandle, itemLayout):
"""Set the nwItemLayout for a specific file.
"""
@@ -358,30 +367,6 @@ class NWTree():
return True
##
# Getters
##
def countTypes(self):
"""Count the number of files, folders and roots in the project.
"""
nRoot = 0
nFolder = 0
nFile = 0
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
elif tItem.itemType == nwItemType.ROOT:
nRoot += 1
elif tItem.itemType == nwItemType.FOLDER:
nFolder += 1
elif tItem.itemType == nwItemType.FILE:
nFile += 1
return nRoot, nFolder, nFile
##
# Meta Methods
##
@@ -420,7 +405,7 @@ class NWTree():
return
if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle)
del self._treeRoots[tHandle]
if tHandle == self._trashRoot:
self._trashRoot = None
if tHandle == self._archRoot:
@@ -468,29 +453,16 @@ class NWTree():
self.theProject.setProjectChanged(True)
return
def _makeHandle(self, addSeed=""):
def _makeHandle(self):
"""Generate a unique item handle. In the event that the key
already exists, salt the seed and generate a new handle.
A key collision is very unlikely to be caused by the truncation
of the sha256 hash to 13 characters. Assuming it is near-random,
it will on average happen every 4.5^15 times. However, the clock
seed is likely to occasionally generate a collision if the
handle requests come faster than the clock resolution.
already exists, generate a new one.
"""
if self._handleSeed is None:
newSeed = "%s_%d_%s" % (str(time()), self._handleCount, addSeed)
self._handleCount += 1
else:
# This is used for debugging
newSeed = str(self._handleSeed)
self._handleSeed += 1
logger.verbose("Generating handle with seed '%s'", newSeed)
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
if itemHandle in self._projTree:
logger.verbose("Generating new handle")
handle = f"{random.getrandbits(52):013x}"
if handle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...")
itemHandle = self._makeHandle(addSeed+"!")
handle = self._makeHandle()
return itemHandle
return handle
# END Class NWTree