Merge pull request #257 from vkbo/dev

Dev to Master for Release 0.7
This commit is contained in:
Veronica K. Berglyd Olsen
2020-05-30 00:31:51 +02:00
committed by GitHub
64 changed files with 866 additions and 425 deletions
+4 -6
View File
@@ -14,12 +14,10 @@ docs/source/_*
__pycache__ __pycache__
# Sample Project # Sample Project
sample/**/cache sample/cache
sample/**/wordlist.txt sample/meta
sample/**/sessionInfo.log sample/*.bak
sample/**/*.bak sample/*.lock
sample/**/*.json
sample/**/*.lock
# PyTest # PyTest
tests/temp tests/temp
+22 -1
View File
@@ -1,5 +1,25 @@
# novelWriter ChangeLog # novelWriter ChangeLog
## Version 0.7 RC1 [2020-xx-xx]
**User Interface**
* The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234.
* Clicking a tag now tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234.
* Moved the Close button on the "Build Novel project" dialog to the area with the other buttons since we anyway increased the size of that area. PR #256.
**Project Structure**
* The project folder structure has been simplified and cleaned up. We also now freeze the main entry values in the main XML file. The XML file is now given version 1.1, and no further core changes to its structure will be made without bumping this version. We're also locking it to only be opened by version 0.7 or later. An old project file is converted on first open. PRs #253 and #261.
* When a project is closed, two table of contents files are written to the project folder. They are named `ToC.txt` and `ToC.json` and are there for the user's convenience if they want to find a specific file from the project in the data folders. As discussed in Issue #259, PR #261.
* The expanded node flag from the project tree was also saved for file entries, which cannot actually be expanded. These flags are no longer saved in the XML file. PR #261.
**Other Changes**
* Dropped the usage of .bak copies of document files. This was the old method to ensure the document data was written successfully, but it uses twice the storage space. Instead, writing via a temp file is the safe way to save files. PR #248.
* The project class now records the accumulated time in seconds a project has been opened. This data is not yet displayed anywhere, but it is being tracked in the project XML file. PR #261.
## Version 0.6.4 [2020-xx-xx] ## Version 0.6.4 [2020-xx-xx]
**User Interface** **User Interface**
@@ -27,7 +47,8 @@
## Version 0.6.2 [2020-05-28] ## Version 0.6.2 [2020-05-28]
* Botched release. Replaced with 0.6.3. Crashes when Build Novel project is opened. * Botched release. Replaced with 0.6.3. Crashes when Build Novel Project is opened.
## Version 0.6.1 [2020-05-25] ## Version 0.6.1 [2020-05-25]
+2 -2
View File
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen"
# The short X.Y version # The short X.Y version
version = "0.6.3" version = "0.7.0"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = "0.6.3" release = "0.7.0rc1"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
+2 -2
View File
@@ -30,9 +30,9 @@ The project XML file is indent-formatted, suitable for diff tools and version co
Project Documents Project Documents
----------------- -----------------
The project documents are saved in folders starting with ``data_``. The project documents are saved in a folder in the main project folder named ``content``.
Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created. Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created.
The documents are saved with a folder and filename derived from this hash. The documents are saved with a filename assembled from this hash and the file extension ``.nwd``.
If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open. If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open.
The reason for this cryptic file naming is to avoid issues with file naming conventions and restrictions on different operating systems, and also to have a file name that does not depend on what the user names the files, or changes it to. The reason for this cryptic file naming is to avoid issues with file naming conventions and restrictions on different operating systems, and also to have a file name that does not depend on what the user names the files, or changes it to.
+2 -2
View File
@@ -40,8 +40,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen" __copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3" __license__ = "GPLv3"
__version__ = "0.6.3" __version__ = "0.7.0rc1"
__hexversion__ = "0x000603f0" __hexversion__ = "0x000700c1"
__date__ = "2020-05-28" __date__ = "2020-05-28"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
+3 -1
View File
@@ -533,7 +533,9 @@ class Config:
# Write config file # Write config file
try: try:
cnfParse.write(open(path.join(self.confPath,self.confFile),mode="w",encoding="utf8")) cnfParse.write(
open(path.join(self.confPath, self.confFile), mode="w", encoding="utf8")
)
self.confChanged = False self.confChanged = False
except Exception as e: except Exception as e:
logger.error("Could not save config file") logger.error("Could not save config file")
+2
View File
@@ -39,6 +39,8 @@ class nwFiles():
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock" PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
TOC_JSON = "ToC.json"
SESS_INFO = "sessionInfo.log" SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json" OPTS_FILE = "guiOptions.json"
-2
View File
@@ -9,7 +9,6 @@ from nw.core.spellcheck import NWSpellSimple
from nw.core.tokenizer import Tokenizer from nw.core.tokenizer import Tokenizer
from nw.core.tohtml import ToHtml from nw.core.tohtml import ToHtml
from nw.core.tools import countWords from nw.core.tools import countWords
from nw.core.tools import projectMaintenance
from nw.core.tools import numberToWord from nw.core.tools import numberToWord
__all__ = [ __all__ = [
@@ -22,6 +21,5 @@ __all__ = [
"Tokenizer", "Tokenizer",
"ToHtml", "ToHtml",
"countWords", "countWords",
"projectMaintenance",
"numberToWord", "numberToWord",
] ]
+24 -44
View File
@@ -37,8 +37,6 @@ logger = logging.getLogger(__name__)
class NWDoc(): class NWDoc():
FILE_MN = "main.nwd"
def __init__(self, theProject, theParent): def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
@@ -93,17 +91,17 @@ class NWDoc():
if self.theItem.parHandle == self.theProject.projTree.trashRoot(): if self.theItem.parHandle == self.theProject.projTree.trashRoot():
self.docEditable = False self.docEditable = False
docDir, docFile = self._assemblePath(self.docHandle, self.FILE_MN) docFile = self.docHandle+".nwd"
self.fileLoc = path.join(docDir,docFile) logger.debug("Opening document %s" % docFile)
logger.debug("Opening document %s" % self.fileLoc)
dataDir = path.join(self.theProject.projPath, docDir) docPath = path.join(self.theProject.projContent, docFile)
docPath = path.join(dataDir, docFile) self.fileLoc = docPath
theText = "" theText = ""
self.docMeta = "" self.docMeta = ""
if path.isfile(docPath): if path.isfile(docPath):
try: try:
with open(docPath,mode="r",encoding="utf8") as inFile: with open(docPath, mode="r", encoding="utf8") as inFile:
fstLine = inFile.readline() fstLine = inFile.readline()
if fstLine.startswith("%%~ "): if fstLine.startswith("%%~ "):
# This is the meta line # This is the meta line
@@ -113,7 +111,7 @@ class NWDoc():
theText += inFile.read() theText += inFile.read()
except Exception as e: except Exception as e:
self.makeAlert(["Failed to open document file.",str(e)], nwAlert.ERROR) self.makeAlert(["Failed to open document file.", str(e)], nwAlert.ERROR)
# Note: Document must be cleared in case of an io error, # Note: Document must be cleared in case of an io error,
# or else the auto-save or save will try to overwrite it # or else the auto-save or save will try to overwrite it
# with an empty file. Return None to alert the caller. # with an empty file. Return None to alert the caller.
@@ -139,34 +137,29 @@ class NWDoc():
if self.docHandle is None or not self.docEditable: if self.docHandle is None or not self.docEditable:
return False return False
docDir, docFile = self._assemblePath(self.docHandle, self.FILE_MN) self.theProject.ensureFolderStructure()
logger.debug("Saving document %s" % path.join(docDir,docFile))
dataPath = path.join(self.theProject.projPath, docDir)
docPath = path.join(dataPath, docFile)
if not path.isdir(dataPath):
mkdir(dataPath)
logger.debug("Created folder %s" % dataPath)
docTemp = path.join(dataPath, docFile+"~") docFile = self.docHandle+".nwd"
docBack = path.join(dataPath, docFile[:-3]+"bak") logger.debug("Saving document %s" % docFile)
docPath = path.join(self.theProject.projContent, docFile)
docTemp = path.join(self.theProject.projContent, docFile+"~")
itemPath = self.theProject.projTree.getItemPath(self.docHandle) itemPath = self.theProject.projTree.getItemPath(self.docHandle)
docMeta = "%%~ "+":".join(itemPath)+":"+self.theItem.itemName+"\n" docMeta = "%%~ "+":".join(itemPath)+":"+self.theItem.itemName+"\n"
try: try:
with open(docTemp,mode="w",encoding="utf8") as outFile: with open(docTemp, mode="w", encoding="utf8") as outFile:
outFile.write(docMeta) outFile.write(docMeta)
outFile.write(docText) outFile.write(docText)
except Exception as e: except Exception as e:
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR)
return False return False
# If we're here, the file was successfully saved, # If we're here, the file was successfully saved, so we can
# so let's sort out the temps and backups # replace the temp file with the actual file
if path.isfile(docBack):
unlink(docBack)
if path.isfile(docPath): if path.isfile(docPath):
rename(docPath, docBack) unlink(docPath)
rename(docTemp, docPath) rename(docTemp, docPath)
self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName) self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName)
@@ -177,12 +170,12 @@ class NWDoc():
"""Permanently delete a document source file and its backups """Permanently delete a document source file and its backups
from the project data folder. from the project data folder.
""" """
docDir, docFile = self._assemblePath(tHandle, self.FILE_MN) docFile = self.docHandle+".nwd"
dataPath = path.join(self.theProject.projPath, docDir)
chkList = [] chkList = []
chkList.append(path.join(dataPath, docFile)) chkList.append(path.join(self.theProject.projContent, docFile))
chkList.append(path.join(dataPath, docFile+"~")) chkList.append(path.join(self.theProject.projContent, docFile+"~"))
chkList.append(path.join(dataPath, docFile[:-3]+"bak"))
for chkFile in chkList: for chkFile in chkList:
if path.isfile(chkFile): if path.isfile(chkFile):
try: try:
@@ -191,6 +184,7 @@ class NWDoc():
except Exception as e: except Exception as e:
self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR) self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR)
return False return False
return True return True
## ##
@@ -224,18 +218,4 @@ class NWDoc():
return theMeta, thePath return theMeta, thePath
##
# Internal Functions
##
@staticmethod
def _assemblePath(tHandle, docExt):
"""Assemble the file path for a given handle.
"""
if tHandle is None:
return None, None
docDir = "data_"+tHandle[0]
docFile = tHandle[1:13]+"_"+docExt
return docDir, docFile
# END Class NWDoc # END Class NWDoc
+17 -17
View File
@@ -199,7 +199,7 @@ class NWIndex():
try: try:
for tTag in self.tagIndex: for tTag in self.tagIndex:
if len(self.tagIndex[tTag]) != 3: if len(self.tagIndex[tTag]) != 4:
self.indexBroken = True self.indexBroken = True
for tHandle in self.refIndex: for tHandle in self.refIndex:
@@ -228,7 +228,7 @@ class NWIndex():
if self.indexBroken: if self.indexBroken:
self.clearIndex() self.clearIndex()
self.theParent.makeAlert( self.theParent.makeAlert(
"The index loaded from project cache contains errors. Rebuilding index.", "The project index is outdated or broken. Rebuilding index.",
nwAlert.WARN nwAlert.WARN
) )
@@ -260,9 +260,9 @@ class NWIndex():
logger.debug("Indexing item with handle %s" % tHandle) logger.debug("Indexing item with handle %s" % tHandle)
# Check file type, and reset its old index # Check file type, and reset its old index
# Also add a dummy entry for T0 in case the file has no title # Also add a dummy entry T000000 in case the file has no title
self.refIndex[tHandle] = {} self.refIndex[tHandle] = {}
self.refIndex[tHandle]["T0"] = { self.refIndex[tHandle]["T000000"] = {
"tags" : [], "tags" : [],
"updated" : time(), "updated" : time(),
} }
@@ -301,7 +301,7 @@ class NWIndex():
elif aLine.startswith(r"@"): elif aLine.startswith(r"@"):
self._indexNoteRef(tHandle, aLine, nLine, nTitle) self._indexNoteRef(tHandle, aLine, nLine, nTitle)
self._indexTag(tHandle, aLine, nLine, itemClass) self._indexTag(tHandle, aLine, nLine, nTitle, itemClass)
elif aLine.startswith(r"%"): elif aLine.startswith(r"%"):
if nTitle > 0: if nTitle > 0:
@@ -436,7 +436,7 @@ class NWIndex():
return True return True
def _indexTag(self, tHandle, aLine, nLine, itemClass): def _indexTag(self, tHandle, aLine, nLine, nTitle, itemClass):
"""Validate and save the information from a tag. """Validate and save the information from a tag.
""" """
isValid, theBits, thePos = self.scanThis(aLine) isValid, theBits, thePos = self.scanThis(aLine)
@@ -444,7 +444,8 @@ class NWIndex():
return False return False
if theBits[0] == nwKeyWords.TAG_KEY: if theBits[0] == nwKeyWords.TAG_KEY:
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name] sTitle = "T%06d" % nTitle
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
return True return True
@@ -606,18 +607,17 @@ class NWIndex():
if tHandle is None: if tHandle is None:
return theRefs return theRefs
theTag = None theTags = set()
for tTag in self.tagIndex: for tTag in self.tagIndex:
if tHandle == self.tagIndex[tTag][1]: if tHandle == self.tagIndex[tTag][1]:
theTag = tTag theTags.add(tTag)
break
if theTag is not None: if theTags:
for tHandle in self.refIndex: for tHandle in self.refIndex:
for sTitle in self.refIndex[tHandle]: for sTitle in self.refIndex[tHandle]:
for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]:
if tTag == theTag: if tTag in theTags and tHandle not in theRefs:
theRefs[tHandle] = nLine theRefs[tHandle] = sTitle
return theRefs return theRefs
@@ -626,8 +626,8 @@ class NWIndex():
""" """
if theTag in self.tagIndex: if theTag in self.tagIndex:
theRef = self.tagIndex[theTag] theRef = self.tagIndex[theTag]
if len(theRef) == 3: if len(theRef) == 4:
return theRef[1], theRef[0] return theRef[1], theRef[0], theRef[3]
return None, 0 return None, 0, "T000000"
# END Class NWIndex # END Class NWIndex
+281 -53
View File
@@ -31,9 +31,10 @@
""" """
import logging import logging
import json
import nw import nw
from os import path, mkdir, listdir, unlink, rename from os import path, mkdir, listdir, unlink, rename, rmdir
from lxml import etree from lxml import etree
from hashlib import sha256 from hashlib import sha256
from time import time from time import time
@@ -42,7 +43,6 @@ from shutil import make_archive
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from nw.gui.tools import OptionState from nw.gui.tools import OptionState
from nw.core.tools import projectMaintenance
from nw.core.document import NWDoc from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import ( from nw.constants import (
@@ -70,12 +70,15 @@ class NWProject():
self.lockedBy = None # Data on which computer has the project open self.lockedBy = None # Data on which computer has the project open
self.saveCount = 0 # Meta data: number of saves self.saveCount = 0 # Meta data: number of saves
self.autoCount = 0 # Meta data: number of automatic saves self.autoCount = 0 # Meta data: number of automatic saves
self.editTime = 0 # The accumulated edit time read from the project file
# Class Settings # Class Settings
self.projPath = None # The full path to where the currently open project is saved self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder self.projMeta = None # The full path to the project's meta data folder
self.projDict = None # The spell check dictionary self.projCache = None # The full path to the project's cache folder
self.projFile = None # The file name of the project main XML file self.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main XML file
# Project Meta # Project Meta
self.projName = "" # Project name (working title) self.projName = "" # Project name (working title)
@@ -112,7 +115,7 @@ class NWProject():
CUSTOM, and always have parent handle set to None. CUSTOM, and always have parent handle set to None.
""" """
if not self.projTree.checkRootUnique(rootClass): if not self.projTree.checkRootUnique(rootClass):
self.makeAlert("Duplicate root item detected!", nwAlert.ERROR) self.makeAlert("Duplicate root item detected.", nwAlert.ERROR)
return None return None
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(rootName) newItem.setName(rootName)
@@ -195,6 +198,8 @@ class NWProject():
# Project Settings # Project Settings
self.projPath = None self.projPath = None
self.projMeta = None self.projMeta = None
self.projCache = None
self.projContent = None
self.projDict = None self.projDict = None
self.projFile = nwFiles.PROJ_FILE self.projFile = nwFiles.PROJ_FILE
self.projName = "" self.projName = ""
@@ -203,7 +208,7 @@ class NWProject():
self.autoReplace = {} self.autoReplace = {}
self.titleFormat = { self.titleFormat = {
"title" : r"%title%", "title" : r"%title%",
"chapter" : r"Chapter %num%\\%title%", "chapter" : r"Chapter %ch%: %title%",
"unnumbered" : r"%title%", "unnumbered" : r"%title%",
"scene" : r"* * *", "scene" : r"* * *",
"section" : r"", "section" : r"",
@@ -246,11 +251,30 @@ class NWProject():
self.projPath = path.abspath(path.dirname(fileName)) self.projPath = path.abspath(path.dirname(fileName))
logger.debug("Opening project: %s" % self.projPath) logger.debug("Opening project: %s" % self.projPath)
self.projMeta = path.join(self.projPath,"meta") # Standard Folders and Files
# ==========================
if not self.ensureFolderStructure():
return False
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
if not self._checkFolder(self.projMeta): # Check for Old Legacy Data
return False # =========================
errList = []
for projItem in listdir(self.projPath):
logger.verbose("Project contains: %s" % projItem)
if projItem.startswith("data_"):
self._legacyDataFolder(projItem)
if errList:
self.makeAlert(errList, nwAlert.ERROR)
self._deprecatedFiles()
# Project Lock
# ============
if overrideLock: if overrideLock:
self._clearLockFile() self._clearLockFile()
@@ -267,10 +291,8 @@ class NWProject():
else: else:
logger.verbose("Project is not locked") logger.verbose("Project is not locked")
try: # Open The Project XML File
projectMaintenance(self) # =========================
except Exception as E:
logger.error(str(E))
try: try:
nwXML = etree.parse(fileName) nwXML = etree.parse(fileName)
@@ -291,7 +313,7 @@ class NWProject():
self.clearProject() self.clearProject()
return False return False
xRoot = nwXML.getroot() xRoot = nwXML.getroot()
nwxRoot = xRoot.tag nwxRoot = xRoot.tag
appVersion = "Unknown" appVersion = "Unknown"
@@ -310,17 +332,51 @@ class NWProject():
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False) self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
if "autoCount" in xRoot.attrib: if "autoCount" in xRoot.attrib:
self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False) self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False)
if "editTime" in xRoot.attrib:
self.editTime = checkInt(xRoot.attrib["editTime"], 0, False)
logger.verbose("XML root is %s" % nwxRoot) logger.verbose("XML root is %s" % nwxRoot)
logger.verbose("File version is %s" % fileVersion) logger.verbose("File version is %s" % fileVersion)
if not nwxRoot == "novelWriterXML" or not fileVersion == "1.0": # Check File Type
# ===============
if not nwxRoot == "novelWriterXML":
self.makeAlert( self.makeAlert(
"Project file does not appear to be a novelWriterXML file version 1.0", "Project file does not appear to be a novelWriterXML file.",
nwAlert.ERROR nwAlert.ERROR
) )
return False return False
# Check Project Storage Version
# =============================
if fileVersion == "1.0":
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", (
"The project file and data is created by a %s version lower than 0.7. "
"Do you want to upgrade the project to the most recent format?<br><br>"
"Note that after the upgrade, you cannot open the project with an older "
"version of %s any more, so make sure you have a recent backup."
) % (
nw.__package__, nw.__package__
))
if msgRes == QMessageBox.Yes:
self._updateStorage()
else:
return False
elif fileVersion != "1.1":
self.makeAlert((
"Unknown or unsupported %s project format. "
"The project cannot be opened by this version of %s."
) % (
nw.__package__, nw.__package__
), nwAlert.ERROR)
return False
# Check novelWriter Version
# =========================
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Version Conflict", ( msgRes = msgBox.question(self.theParent, "Version Conflict", (
@@ -333,6 +389,9 @@ class NWProject():
if msgRes != QMessageBox.Yes: if msgRes != QMessageBox.Yes:
return False return False
# Start Parsing XML
# =================
for xChild in xRoot: for xChild in xRoot:
if xChild.tag == "project": if xChild.tag == "project":
logger.debug("Found project meta") logger.debug("Found project meta")
@@ -350,6 +409,7 @@ class NWProject():
self.bookAuthors.append(xItem.text) self.bookAuthors.append(xItem.text)
elif xItem.tag == "backup": elif xItem.tag == "backup":
self.doBackup = checkBool(xItem.text, False) self.doBackup = checkBool(xItem.text, False)
elif xChild.tag == "settings": elif xChild.tag == "settings":
logger.debug("Found project settings") logger.debug("Found project settings")
for xItem in xChild: for xItem in xChild:
@@ -377,6 +437,7 @@ class NWProject():
for xEntry in xItem: for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False) titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
self.setTitleFormat(titleFormat) self.setTitleFormat(titleFormat)
elif xChild.tag == "content": elif xChild.tag == "content":
logger.debug("Found project content") logger.debug("Found project content")
self.projTree.unpackXML(xChild) self.projTree.unpackXML(xChild)
@@ -404,15 +465,13 @@ class NWProject():
file. file.
""" """
if self.projPath is None: if self.projPath is None:
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR) self.makeAlert(
"Project path not set, cannot save project.", nwAlert.ERROR
)
return False return False
self.projMeta = path.join(self.projPath,"meta")
saveTime = time() saveTime = time()
if not self.ensureFolderStructure():
if not self._checkFolder(self.projPath):
return False
if not self._checkFolder(self.projMeta):
return False return False
logger.debug("Saving project: %s" % self.projPath) logger.debug("Saving project: %s" % self.projPath)
@@ -427,10 +486,11 @@ class NWProject():
nwXML = etree.Element("novelWriterXML",attrib={ nwXML = etree.Element("novelWriterXML",attrib={
"appVersion" : str(nw.__version__), "appVersion" : str(nw.__version__),
"hexVersion" : str(nw.__hexversion__), "hexVersion" : str(nw.__hexversion__),
"fileVersion" : "1.0", "fileVersion" : "1.1",
"saveCount" : str(self.saveCount), "saveCount" : str(self.saveCount),
"autoCount" : str(self.autoCount), "autoCount" : str(self.autoCount),
"timeStamp" : formatTimeStamp(saveTime), "timeStamp" : formatTimeStamp(saveTime),
"editTime" : str(int(self.editTime + saveTime - self.projOpened)),
}) })
# Save Project Meta # Save Project Meta
@@ -480,7 +540,7 @@ class NWProject():
xml_declaration = True xml_declaration = True
)) ))
except Exception as e: except Exception as e:
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR) self.makeAlert(["Failed to save project.", str(e)], nwAlert.ERROR)
return False return False
# If we're here, the file was successfully saved, # If we're here, the file was successfully saved,
@@ -507,12 +567,33 @@ class NWProject():
def closeProject(self): def closeProject(self):
"""Close the current project and clear all meta data. """Close the current project and clear all meta data.
""" """
self.projTree.writeToCFiles()
self._appendSessionStats() self._appendSessionStats()
self._clearLockFile() self._clearLockFile()
self.clearProject() self.clearProject()
self.lockedBy = None self.lockedBy = None
return True return True
def ensureFolderStructure(self):
"""Ensure that all necessary folders exist in the project
folder.
"""
if self.projPath is None or self.projPath == "":
return False
self.projMeta = path.join(self.projPath, "meta")
self.projCache = path.join(self.projPath, "cache")
self.projContent = path.join(self.projPath, "content")
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projCache):
return False
if not self._checkFolder(self.projContent):
return False
return True
## ##
# Backup Project # Backup Project
## ##
@@ -937,35 +1018,26 @@ class NWProject():
if self.projPath is None: if self.projPath is None:
return return
# First, scan the project data folders # Then check the files in the data folder
itemList = []
for subItem in listdir(self.projPath):
if subItem[:5] != "data_":
continue
dataDir = path.join(self.projPath,subItem)
for subFile in listdir(dataDir):
if subFile[-4:] == ".nwd":
newItem = path.join(subItem,subFile)
itemList.append(newItem)
# Then check the valid files
orphanFiles = [] orphanFiles = []
for fileItem in itemList: for fileItem in listdir(self.projContent):
if len(fileItem) != 28: if not fileItem.endswith(".nwd"):
# Just to be safe, shouldn't happen
logger.warning("Skipping file %s" % fileItem) logger.warning("Skipping file %s" % fileItem)
continue continue
fHandle = fileItem[5]+fileItem[7:19] if len(fileItem) != 17:
logger.warning("Skipping file %s" % fileItem)
continue
fHandle = fileItem[:13]
if fHandle in self.projTree: if fHandle in self.projTree:
logger.debug("Checking file %s, handle %s: OK" % (fileItem,fHandle)) logger.debug("Checking file %s, handle %s: OK" % (fileItem, fHandle))
else: else:
logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem,fHandle)) logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem, fHandle))
orphanFiles.append(fHandle) orphanFiles.append(fHandle)
# Report status # Report status
if len(orphanFiles) > 0: if len(orphanFiles) > 0:
self.makeAlert( self.makeAlert(
"Found %d orphaned file(s) in project folder!" % len(orphanFiles), "Found %d orphaned file(s) in project folder." % len(orphanFiles),
nwAlert.WARN nwAlert.WARN
) )
else: else:
@@ -998,7 +1070,7 @@ class NWProject():
def _appendSessionStats(self): def _appendSessionStats(self):
"""Append session statistics to the sessions log file. """Append session statistics to the sessions log file.
""" """
if self.projMeta is None: if not self.ensureFolderStructure():
return False return False
sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO) sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO)
@@ -1016,12 +1088,122 @@ class NWProject():
return True return True
##
# Legacy Data Structure Handlers
##
def _legacyDataFolder(self, theFolder):
"""Clean up legacy data folders.
"""
errList = []
theData = path.join(self.projPath, theFolder)
if not path.isdir(theData):
errList.append("Not a folder: %s" % theData)
return errList
logger.info("Old data folder %s found" % theFolder)
# Move Documents to Content
# =========================
for dataItem in listdir(theData):
theFile = path.join(theData, dataItem)
if not path.isfile(theFile):
theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
continue
if len(dataItem) == 21 and dataItem.endswith("_main.nwd"):
tHandle = theFolder[-1]+dataItem[:12]
newPath = path.join(self.projContent, tHandle+".nwd")
try:
rename(theFile, newPath)
logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath)
except Exception as e:
logger.error(str(e))
errList.append("Could not move: %s" % theFile)
elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
try:
unlink(theFile)
logger.info("Deleted file: %s" % theFile)
except Exception as e:
logger.error(str(e))
errList.append("Could not delete: %s" % theFile)
else:
theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
# Remove Data Folder
# ==================
try:
rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
except:
errList.append("Failed to remove: %s" % theFolder)
return errList
def _moveUnknownItem(self, theDir, theItem):
"""Move an item that doesn't belong in the project folder to
a junk folder.
"""
theJunk = path.join(self.projPath, "junk")
if not self._checkFolder(theJunk):
return "Could not make folder: %s" % theJunk
theSrc = path.join(theDir, theItem)
theDst = path.join(theJunk, theItem)
try:
rename(theSrc, theDst)
logger.info("Moved to junk: %s" % theSrc)
except Exception as e:
logger.error(str(e))
return "Could not move item %s to junk." % theSrc
return ""
def _deprecatedFiles(self):
"""Delete files that are no longer used by novelWriter.
"""
rmList = []
rmList.append(path.join(self.projCache, "nwProject.nwx.0"))
rmList.append(path.join(self.projCache, "nwProject.nwx.1"))
rmList.append(path.join(self.projCache, "nwProject.nwx.2"))
rmList.append(path.join(self.projCache, "nwProject.nwx.3"))
rmList.append(path.join(self.projCache, "nwProject.nwx.4"))
rmList.append(path.join(self.projCache, "nwProject.nwx.5"))
rmList.append(path.join(self.projCache, "nwProject.nwx.6"))
rmList.append(path.join(self.projCache, "nwProject.nwx.7"))
rmList.append(path.join(self.projCache, "nwProject.nwx.8"))
rmList.append(path.join(self.projCache, "nwProject.nwx.9"))
rmList.append(path.join(self.projMeta, "mainOptions.json"))
rmList.append(path.join(self.projMeta, "exportOptions.json"))
rmList.append(path.join(self.projMeta, "outlineOptions.json"))
rmList.append(path.join(self.projMeta, "timelineOptions.json"))
rmList.append(path.join(self.projMeta, "docMergeOptions.json"))
rmList.append(path.join(self.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
# END Class NWProject # END Class NWProject
# ================================================================================================ # # =============================================================================================== #
# NWTree # NWTree
# Class holding the project tree for the NWProject # Class holding the project tree for the NWProject
# ================================================================================================ # # =============================================================================================== #
class NWTree(): class NWTree():
@@ -1120,6 +1302,51 @@ class NWTree():
return True return True
def writeToCFiles(self):
"""Write the convenience table of contents files in the root of
the project directory. These files are there to assist the user
if they wish to browse the stored files.
"""
tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
jsonData = []
try:
# Dump the text
with open(tocText, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
outFile.write(" Table of Contents\n")
outFile.write("===================\n")
outFile.write("\n")
outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label"))
outFile.write("-"*80+"\n")
for tHandle in sorted(self._treeOrder):
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
tFile = tHandle+".nwd"
if path.isfile(path.join(self.theProject.projContent, tFile)):
outFile.write(" %-25s %-9s %s\n" %(
path.join("content", tFile),
tItem.itemClass.name,
tItem.itemName,
))
jsonData.append([
path.join("content", tFile),
tItem.itemClass.name,
tItem.itemName,
])
outFile.write("\n")
# Dump the JSON
with open(tocJson, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(jsonData, indent=2))
except Exception as e:
logger.error(str(e))
return
## ##
# Tree Structure Methods # Tree Structure Methods
## ##
@@ -1356,10 +1583,10 @@ class NWTree():
# END Class NWTree # END Class NWTree
# ================================================================================================ # # =============================================================================================== #
# NWItem # NWItem
# Class holding the project items making up the NWProject # Class holding the project items making up the NWProject
# ================================================================================================ # # =============================================================================================== #
class NWItem(): class NWItem():
@@ -1402,7 +1629,6 @@ class NWItem():
xSub = self._subPack(xPack,"type", text=str(self.itemType.name)) xSub = self._subPack(xPack,"type", text=str(self.itemType.name))
xSub = self._subPack(xPack,"class", text=str(self.itemClass.name)) xSub = self._subPack(xPack,"class", text=str(self.itemClass.name))
xSub = self._subPack(xPack,"status", text=str(self.itemStatus)) xSub = self._subPack(xPack,"status", text=str(self.itemStatus))
xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
if self.itemType == nwItemType.FILE: if self.itemType == nwItemType.FILE:
xSub = self._subPack(xPack,"exported", text=str(self.isExported)) xSub = self._subPack(xPack,"exported", text=str(self.isExported))
xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name)) xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name))
@@ -1410,6 +1636,8 @@ class NWItem():
xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False) xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False)
xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False) xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False)
xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False) xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False)
else:
xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
return return
def unpackXML(self, xItem): def unpackXML(self, xItem):
@@ -1601,10 +1829,10 @@ class NWItem():
# END Class NWItem # END Class NWItem
# ================================================================================================ # # =============================================================================================== #
# NWStatus # NWStatus
# Class holding the item status values stored in the NWProject # Class holding the item status values stored in the NWProject
# ================================================================================================ # # =============================================================================================== #
class NWStatus(): class NWStatus():
+6 -6
View File
@@ -35,9 +35,9 @@ from nw.constants import isoLanguage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ================================================================================================ # # =============================================================================================== #
# SpellChecking SuperClass # SpellChecking SuperClass
# ================================================================================================ # # =============================================================================================== #
class NWSpellCheck(): class NWSpellCheck():
@@ -129,9 +129,9 @@ class NWSpellCheck():
# END Class NWSpellCheck # END Class NWSpellCheck
# ================================================================================================ # # =============================================================================================== #
# Enchant Based SpellChecking # Enchant Based SpellChecking
# ================================================================================================ # # =============================================================================================== #
class NWSpellEnchant(NWSpellCheck): class NWSpellEnchant(NWSpellCheck):
@@ -211,9 +211,9 @@ class NWSpellEnchantDummy:
# END Class NWSpellEnchantDummy # END Class NWSpellEnchantDummy
# ================================================================================================ # # =============================================================================================== #
# Fallback SpellChecking Using difflib # Fallback SpellChecking Using difflib
# ================================================================================================ # # =============================================================================================== #
class NWSpellSimple(NWSpellCheck): class NWSpellSimple(NWSpellCheck):
"""Internal spell check tool that uses standard Python packages with """Internal spell check tool that uses standard Python packages with
+21 -16
View File
@@ -143,7 +143,7 @@ class ToHtml(Tokenizer):
parStyle = None parStyle = None
tmpResult = [] tmpResult = []
hasHardBreak = False hasHardBreak = False
for tType, tText, tFormat, tStyle in self.theTokens: for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Styles # Styles
aStyle = [] aStyle = []
@@ -174,6 +174,11 @@ class ToHtml(Tokenizer):
else: else:
hStyle = "" hStyle = ""
if self.linkHeaders:
aNm = "<a name='head_%s:T%06d'></a>" % (self.theHandle, tLine)
else:
aNm = ""
# Process TextType # Process TextType
if tType == self.T_EMPTY: if tType == self.T_EMPTY:
if parStyle is None: if parStyle is None:
@@ -191,23 +196,23 @@ class ToHtml(Tokenizer):
elif tType == self.T_TITLE: elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1 class='title'%s>%s</h1>\n" % (hStyle, tHead)) tmpResult.append("<h1 class='title'%s>%s%s</h1>\n" % (hStyle, aNm, tHead))
elif tType == self.T_HEAD1: elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h1, hStyle, tHead, h1)) tmpResult.append("<%s%s>%s%s</%s>\n" % (h1, hStyle, aNm, tHead, h1))
elif tType == self.T_HEAD2: elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h2, hStyle, tHead, h2)) tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
elif tType == self.T_HEAD3: elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h3, hStyle, tHead, h3)) tmpResult.append("<%s%s>%s%s</%s>\n" % (h3, hStyle, aNm, tHead, h3))
elif tType == self.T_HEAD4: elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s</%s>\n" % (h4, hStyle, tHead, h4)) tmpResult.append("<%s%s>%s%s</%s>\n" % (h4, hStyle, aNm, tHead, h4))
elif tType == self.T_SEP: elif tType == self.T_SEP:
tmpResult.append("<p class='sep'>%s</p>\n" % tText) tmpResult.append("<p class='sep'>%s</p>\n" % tText)
@@ -296,17 +301,17 @@ class ToHtml(Tokenizer):
refTags = [] refTags = []
if theBits[0] in nwLabels.KEY_NAME: if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]] retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
if self.genMode == self.M_PREVIEW: if theBits[0] == nwKeyWords.TAG_KEY:
for tTag in theBits[1:]: retText += "<a name='tag_%s'>%s</a>" % (
refTags.append("<a href='#%s=%s'>%s</a>" % ( theBits[1], theBits[1]
theBits[0][1:], tTag, tTag )
))
retText += ", ".join(refTags)
else: else:
if theBits[0] == nwKeyWords.TAG_KEY: if self.genMode == self.M_PREVIEW:
retText += "<a name='tag_%s'/>%s" % ( for tTag in theBits[1:]:
theBits[1], theBits[1] refTags.append("<a href='#%s=%s'>%s</a>" % (
) theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
else: else:
for tTag in theBits[1:]: for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % ( refTags.append("<a href='#tag_%s'>%s</a>" % (
+144 -51
View File
@@ -102,6 +102,8 @@ class Tokenizer():
self.hideScene = False # Do not include scene headers self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers self.hideSection = False # Do not include section headers
self.linkHeaders = False # Add an anchor before headers
# Instance Variables # Instance Variables
self.numChapter = 0 # Counter for chapter numbers self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter self.numChScene = 0 # Counter for scene number within chapter
@@ -174,6 +176,10 @@ class Tokenizer():
self.hideSection = hideSection self.hideSection = hideSection
return return
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
return
def setBodyText(self, doBodyText): def setBodyText(self, doBodyText):
self.doBodyText = doBodyText self.doBodyText = doBodyText
return return
@@ -307,12 +313,16 @@ class Tokenizer():
self.theTokens = [] self.theTokens = []
self.theMarkdown = "" self.theMarkdown = ""
tmpMarkdown = [] tmpMarkdown = []
nLine = 0
for aLine in self.theText.splitlines(): for aLine in self.theText.splitlines():
nLine += 1
# Tag lines starting with specific characters # Tag lines starting with specific characters
if len(aLine.strip()) == 0: if len(aLine.strip()) == 0:
self.theTokens.append(( self.theTokens.append((
self.T_EMPTY, "", None, self.A_NONE self.T_EMPTY, nLine,
"", None,
self.A_NONE
)) ))
tmpMarkdown.append("\n") tmpMarkdown.append("\n")
@@ -320,45 +330,59 @@ class Tokenizer():
cLine = aLine[1:].strip() cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"): if cLine.lower().startswith("synopsis:"):
self.theTokens.append(( self.theTokens.append((
self.T_SYNOPSIS, cLine[9:].strip(), None, self.A_NONE self.T_SYNOPSIS, nLine,
cLine[9:].strip(), None,
self.A_NONE
)) ))
if self.doSynopsis: if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
else: else:
self.theTokens.append(( self.theTokens.append((
self.T_COMMENT, aLine[1:].strip(), None, self.A_NONE self.T_COMMENT, nLine,
aLine[1:].strip(), None,
self.A_NONE
)) ))
if self.doComments: if self.doComments:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@": elif aLine[0] == "@":
self.theTokens.append(( self.theTokens.append((
self.T_KEYWORD, aLine[1:].strip(), None, self.A_NONE self.T_KEYWORD, nLine,
aLine[1:].strip(), None,
self.A_NONE
)) ))
if self.doKeywords: if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ": elif aLine[:2] == "# ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD1, aLine[2:].strip(), None, self.A_NONE self.T_HEAD1, nLine,
aLine[2:].strip(), None,
self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ": elif aLine[:3] == "## ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD2, aLine[3:].strip(), None, self.A_NONE self.T_HEAD2, nLine,
aLine[3:].strip(), None,
self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ": elif aLine[:4] == "### ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD3, aLine[4:].strip(), None, self.A_NONE self.T_HEAD3, nLine,
aLine[4:].strip(), None,
self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ": elif aLine[:5] == "#### ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD4, aLine[5:].strip(), None, self.A_NONE self.T_HEAD4, nLine,
aLine[5:].strip(), None,
self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
@@ -383,13 +407,17 @@ class Tokenizer():
# sorted by position # sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0)) fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append(( self.theTokens.append((
self.T_TEXT, aLine, fmtPos, self.A_NONE self.T_TEXT, nLine,
aLine, fmtPos,
self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end # Always add an empty line at the end
self.theTokens.append(( self.theTokens.append((
self.T_EMPTY, "", None, self.A_NONE self.T_EMPTY, nLine,
"", None,
self.A_NONE
)) ))
tmpMarkdown.append("\n") tmpMarkdown.append("\n")
@@ -413,100 +441,146 @@ class Tokenizer():
for n in range(len(self.theTokens)): for n in range(len(self.theTokens)):
tToken = self.theTokens[n] tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
# In case we see text before a scene, we reset the flag # In case we see text before a scene, we reset the flag
if tType == self.T_TEXT: if tToken[0] == self.T_TEXT:
self.firstScene = False self.firstScene = False
elif tType == self.T_HEAD1: elif tToken[0] == self.T_HEAD1:
# Main Title # Main Title
# ========== # ==========
tText = self._formatHeading(self.fmtTitle, tText) tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, None, self.A_NONE tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
) )
elif tType == self.T_HEAD2: elif tToken[0] == self.T_HEAD2:
# Novel Chapter # Novel Chapter
# ============= # =============
# Numbered or Unnumbered # Numbered or Unnumbered
if self.isUnNum: if self.isUnNum:
tText = self._formatHeading(self.fmtUnNum, tText) tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
else: else:
self.numChapter += 1 self.numChapter += 1
tText = self._formatHeading(self.fmtChapter, tText) tTemp = self._formatHeading(self.fmtChapter, tToken[2])
# Format the chapter header # Format the chapter header
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, None, self.A_PBB tToken[0],
tToken[1],
tTemp,
None,
self.A_PBB
) )
# Set scene variables # Set scene variables
self.firstScene = True self.firstScene = True
self.numChScene = 0 self.numChScene = 0
elif tType == self.T_HEAD3: elif tToken[0] == self.T_HEAD3:
# Novel Scene # Novel Scene
# =========== # ===========
self.numChScene += 1 self.numChScene += 1
self.numAbsScene += 1 self.numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tText) tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene: if tTemp == "" and self.hideScene:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
) )
elif tTemp == "" and not self.hideScene: elif tTemp == "" and not self.hideScene:
if self.firstScene: if self.firstScene:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_SKIP, "", None, self.A_NONE self.T_SKIP,
tToken[1],
"",
None,
self.A_NONE
) )
elif tTemp == self.fmtScene: elif tTemp == self.fmtScene:
if self.firstScene: if self.firstScene:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE self.T_SEP,
tToken[1],
tTemp,
None,
self.A_CENTRE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tTemp, None, self.A_NONE tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
) )
# Definitely no longer the first scene # Definitely no longer the first scene
self.firstScene = False self.firstScene = False
elif tType == self.T_HEAD4: elif tToken[0] == self.T_HEAD4:
# Novel Section # Novel Section
# ============= # =============
tTemp = self._formatHeading(self.fmtSection, tText) tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection: if tTemp == "" and self.hideSection:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, self.A_NONE self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
) )
elif tTemp == "" and not self.hideSection: elif tTemp == "" and not self.hideSection:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_SKIP, "", None, self.A_NONE self.T_SKIP,
tToken[1],
"",
None,
self.A_NONE
) )
elif tTemp == self.fmtSection: elif tTemp == self.fmtSection:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE self.T_SEP,
tToken[1],
tTemp,
None,
self.A_CENTRE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tTemp, None, self.A_NONE tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
) )
# For title page and partitions, we need to centre all text. # For title page and partitions, we need to centre all text.
@@ -515,21 +589,30 @@ class Tokenizer():
# We also swap header level 1 with a title type instead. # We also swap header level 1 with a title type instead.
if self.isTitle or self.isPart: if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens): for n, tToken in enumerate(self.theTokens):
tType = tToken[0] if tToken[0] == self.T_HEAD1:
tText = tToken[1]
tFormat = tToken[2]
if tType == self.T_HEAD1:
if self.isTitle: if self.isTitle:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_TITLE, tText, tFormat, self.A_PBB_NO | self.A_CENTRE self.T_TITLE,
tToken[1],
tToken[2],
tToken[3],
self.A_PBB_NO | self.A_CENTRE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, tFormat, self.A_PBB | self.A_CENTRE tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_PBB | self.A_CENTRE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, tFormat, self.A_CENTRE tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_CENTRE
) )
# Add a page break after the last entry # Add a page break after the last entry
@@ -537,23 +620,32 @@ class Tokenizer():
if n >= 0: if n >= 0:
tToken = self.theTokens[n] tToken = self.theTokens[n]
self.theTokens[n] = ( self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA tToken[0],
tToken[1],
tToken[2],
tToken[3],
tToken[4] | self.A_PBA
) )
# A single page is always left-aligned and starts on a fresh # A single page is always left-aligned and starts on a fresh
# page, unless it's empty. # page, unless it's empty.
if self.isPage: if self.isPage:
for n, tToken in enumerate(self.theTokens): for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
if n == 0: if n == 0:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, tFormat, self.A_LEFT | self.A_PBB tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_LEFT | self.A_PBB
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, tFormat, self.A_LEFT tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_LEFT
) )
return return
@@ -566,10 +658,11 @@ class Tokenizer():
"""Replaces the %keyword% strings. """Replaces the %keyword% strings.
""" """
theTitle = theTitle.replace(r"%title%", theText) theTitle = theTitle.replace(r"%title%", theText)
theTitle = theTitle.replace(r"%ch%", str(self.numChapter)) theTitle = theTitle.replace(r"%ch%", str(self.numChapter))
theTitle = theTitle.replace(r"%sc%", str(self.numChScene)) theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene)) theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en")) if r"%chw%" in theTitle:
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en"))
return theTitle return theTitle
# END Class Tokenizer # END Class Tokenizer
-45
View File
@@ -8,7 +8,6 @@
File History: File History:
Created: 2019-04-22 [0.0.1] countWords Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Created: 2020-02-13 [0.4.3] projectMaintenance
Merged: 2020-05-08 [0.4.5] All of the above into this file Merged: 2020-05-08 [0.4.5] All of the above into this file
This file is a part of novelWriter This file is a part of novelWriter
@@ -81,50 +80,6 @@ def countWords(theText):
return charCount, wordCount, paraCount return charCount, wordCount, paraCount
def projectMaintenance(theProject):
"""Wrapper class for handling various tasks related to managing old
projects with content from older versions of novelWriter.
"""
# Remove no longer used project cache folder
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
if path.isdir(cacheDir):
logger.info("Deprecated cache folder found")
rmList = []
for i in range(10):
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
rmList.append(path.join(cacheDir, "projCount.txt"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
logger.info("Deleting: %s" % cacheDir)
try:
rmdir(cacheDir)
except Exception as e:
logger.error(str(e))
# Remove no longer used meta files
rmList = []
rmList.append(path.join(theProject.projMeta, "mainOptions.json"))
rmList.append(path.join(theProject.projMeta, "exportOptions.json"))
rmList.append(path.join(theProject.projMeta, "outlineOptions.json"))
rmList.append(path.join(theProject.projMeta, "timelineOptions.json"))
rmList.append(path.join(theProject.projMeta, "docMergeOptions.json"))
rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
def numberToWord(numVal, theLanguage): def numberToWord(numVal, theLanguage):
"""Wrapper for converting numbers to words for chapter headings. """Wrapper for converting numbers to words for chapter headings.
""" """
+2 -2
View File
@@ -9,7 +9,7 @@ from nw.gui.theme import GuiTheme
# Dialogs # Dialogs
from nw.gui.dialogs.about import GuiAbout from nw.gui.dialogs.about import GuiAbout
from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.preferences import GuiPreferences
from nw.gui.dialogs.docmerge import GuiDocMerge from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
@@ -40,7 +40,7 @@ __all__ = [
"GuiMainStatus", "GuiMainStatus",
"GuiTheme", "GuiTheme",
"GuiAbout", "GuiAbout",
"GuiConfigEditor", "GuiPreferences",
"GuiDocMerge", "GuiDocMerge",
"GuiDocSplit", "GuiDocSplit",
"GuiItemEditor", "GuiItemEditor",
+33 -24
View File
@@ -34,12 +34,12 @@ from time import time
from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtCore import Qt, QByteArray
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextOption, QPalette, QColor, QTextDocumentWriter, QFont QPalette, QColor, QTextDocumentWriter, QFont
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
QFileDialog, QFontComboBox, QSpinBox, QDialogButtonBox QFileDialog, QFontComboBox, QSpinBox
) )
from nw.gui.additions import QSwitch from nw.gui.additions import QSwitch
@@ -83,8 +83,7 @@ class GuiBuildNovel(QDialog):
self.optState.getInt("GuiBuildNovel", "winHeight", 800) self.optState.getInt("GuiBuildNovel", "winHeight", 800)
) )
self.outerBox = QVBoxLayout() self.outerBox = QHBoxLayout()
self.innerBox = QHBoxLayout()
self.toolsBox = QVBoxLayout() self.toolsBox = QVBoxLayout()
self.docView = GuiBuildNovelDocView(self, self.theProject) self.docView = GuiBuildNovelDocView(self, self.theProject)
@@ -173,7 +172,9 @@ class GuiBuildNovel(QDialog):
self.textFont = QFontComboBox() self.textFont = QFontComboBox()
self.textFont.setFixedWidth(220) self.textFont.setFixedWidth(220)
self.textFont.setToolTip("The font is used for PDF and printing. Other formats have no font set.") self.textFont.setToolTip(
"The font is used for PDF and printing. Other formats have no font set."
)
self.textFont.setCurrentFont( self.textFont.setCurrentFont(
QFont(self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)) QFont(self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont))
) )
@@ -183,13 +184,17 @@ class GuiBuildNovel(QDialog):
self.textSize.setMinimum(5) self.textSize.setMinimum(5)
self.textSize.setMaximum(48) self.textSize.setMaximum(48)
self.textSize.setSingleStep(1) self.textSize.setSingleStep(1)
self.textSize.setToolTip("The size is used for PDF and printing. Other formats have no size set.") self.textSize.setToolTip(
"The size is used for PDF and printing. Other formats have no size set."
)
self.textSize.setValue( self.textSize.setValue(
self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize) self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize)
) )
self.justifyText = QSwitch() self.justifyText = QSwitch()
self.justifyText.setToolTip("Applies to PDF, printing, HTML, and Open Document exports.") self.justifyText.setToolTip(
"Applies to PDF, printing, HTML, and Open Document exports."
)
self.justifyText.setChecked( self.justifyText.setChecked(
self.optState.getBool("GuiBuildNovel", "justifyText", False) self.optState.getBool("GuiBuildNovel", "justifyText", False)
) )
@@ -211,15 +216,21 @@ class GuiBuildNovel(QDialog):
self.includeGroup.setLayout(self.includeForm) self.includeGroup.setLayout(self.includeForm)
self.includeSynopsis = QSwitch() self.includeSynopsis = QSwitch()
self.includeSynopsis.setToolTip("Include synopsis type comments in the output.") self.includeSynopsis.setToolTip(
"Include synopsis type comments in the output."
)
self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"]) self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"])
self.includeComments = QSwitch() self.includeComments = QSwitch()
self.includeComments.setToolTip("Include plain comments in the output.") self.includeComments.setToolTip(
"Include plain comments in the output."
)
self.includeComments.setChecked(self.theProject.titleFormat["withComments"]) self.includeComments.setChecked(self.theProject.titleFormat["withComments"])
self.includeKeywords = QSwitch() self.includeKeywords = QSwitch()
self.includeKeywords.setToolTip("Include meta keywords (tags, references) in the output.") self.includeKeywords.setToolTip(
"Include meta keywords (tags, references) in the output."
)
self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"]) self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"])
self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0, 1, 1, Qt.AlignLeft) self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0, 1, 1, Qt.AlignLeft)
@@ -292,7 +303,7 @@ class GuiBuildNovel(QDialog):
# Action Buttons # Action Buttons
# ============== # ==============
self.buttonForm = QGridLayout() self.buttonBox = QHBoxLayout()
self.btnPrint = QPushButton("Print") self.btnPrint = QPushButton("Print")
self.btnPrint.clicked.connect(self._printDocument) self.btnPrint.clicked.connect(self._printDocument)
@@ -326,12 +337,13 @@ class GuiBuildNovel(QDialog):
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT) self.saveMenu.addAction(self.saveTXT)
self.buttonForm.addWidget(self.btnSave, 0, 0) self.btnClose = QPushButton("Close")
self.buttonForm.addWidget(self.btnPrint, 0, 1) self.btnClose.clicked.connect(self._doClose)
# Buttons self.buttonBox.addWidget(self.btnSave)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.addWidget(self.btnPrint)
self.buttonBox.rejected.connect(self._doClose) self.buttonBox.addWidget(self.btnClose)
self.buttonBox.setSpacing(4)
# Assemble GUI # Assemble GUI
# ============ # ============
@@ -343,18 +355,15 @@ class GuiBuildNovel(QDialog):
self.toolsBox.addWidget(self.buildProgress) self.toolsBox.addWidget(self.buildProgress)
self.toolsBox.addWidget(self.buildNovel) self.toolsBox.addWidget(self.buildNovel)
self.toolsBox.addSpacing(8) self.toolsBox.addSpacing(8)
self.toolsBox.addLayout(self.buttonForm) self.toolsBox.addLayout(self.buttonBox)
self.innerBox.addLayout(self.toolsBox) self.outerBox.addLayout(self.toolsBox)
self.innerBox.addWidget(self.docView) self.outerBox.addWidget(self.docView)
self.outerBox.setStretch(0, 0)
self.outerBox.setStretch(1, 1)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.innerBox.setStretch(0, 0)
self.innerBox.setStretch(1, 1)
self.show() self.show()
logger.debug("GuiBuildNovel initialisation complete") logger.debug("GuiBuildNovel initialisation complete")
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nw.gui.dialogs.about import GuiAbout from nw.gui.dialogs.about import GuiAbout
from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.preferences import GuiPreferences
from nw.gui.dialogs.docmerge import GuiDocMerge from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
@@ -11,7 +11,7 @@ from nw.gui.dialogs.sessionlog import GuiSessionLogView
__all__ = [ __all__ = [
"GuiAbout", "GuiAbout",
"GuiConfigEditor", "GuiPreferences",
"GuiDocMerge", "GuiDocMerge",
"GuiDocSplit", "GuiDocSplit",
"GuiItemEditor", "GuiItemEditor",
+13 -10
View File
@@ -119,16 +119,19 @@ class GuiAbout(QDialog):
"<h2>About {name:s}</h2>" "<h2>About {name:s}</h2>"
"<p>{copyright:s}.</p>" "<p>{copyright:s}.</p>"
"<p>Website: <a href='{website:s}'>{domain:s}</a></p>" "<p>Website: <a href='{website:s}'>{domain:s}</a></p>"
"<p>{name:s} is a markdown-like text editor designed for organising and writing " "<p>{name:s} is a markdown-like text editor designed for "
"novels. It is written in Python 3 with a Qt5 GUI, using PyQt5.</p>" "organising and writing novels. It is written in Python 3 with a "
"<p>{name:s} is free software: you can redistribute it and/or modify it under the " "Qt5 GUI, using PyQt5.</p>"
"terms of the GNU General Public License as published by the Free Software Foundation, " "<p>{name:s} is free software: you can redistribute it and/or "
"either version 3 of the License, or (at your option) any later version.</p>" "modify it under the terms of the GNU General Public License as "
"<p>{name:s} is distributed in the hope that it will be useful, but WITHOUT ANY " "published by the Free Software Foundation, either version 3 of "
"WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A " "the License, or (at your option) any later version.</p>"
"PARTICULAR PURPOSE.</p>" "<p>{name:s} is distributed in the hope that it will be useful, "
"<p>See the License tab for the full text, or visit the GNU website at " "but WITHOUT ANY WARRANTY; without even the implied warranty of "
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> for more details.</p>" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
"<p>See the License tab for the full text, or visit the GNU website "
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
"for more details.</p>"
"<h3>Credits</h3>" "<h3>Credits</h3>"
"<p>{credits:s}</p>" "<p>{credits:s}</p>"
).format( ).format(
+10 -4
View File
@@ -150,7 +150,9 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return return
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.parHandle
)
self.theParent.treeView.revealTreeItem(fHandle) self.theParent.treeView.revealTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle) logger.verbose("Creating folder %s" % fHandle)
@@ -165,7 +167,7 @@ class GuiDocSplit(QDialog):
elif wTitle.startswith("### "): elif wTitle.startswith("### "):
itemLayout = nwItemLayout.SCENE itemLayout = nwItemLayout.SCENE
elif wTitle.startswith("#### "): elif wTitle.startswith("#### "):
itemLayout = nwItemLayout.PAGE itemLayout = nwItemLayout.SCENE
wTitle = wTitle.lstrip("#") wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip() wTitle = wTitle.strip()
@@ -174,7 +176,9 @@ class GuiDocSplit(QDialog):
newItem = self.theProject.projTree[nHandle] newItem = self.theProject.projTree[nHandle]
newItem.setLayout(itemLayout) newItem.setLayout(itemLayout)
logger.verbose( logger.verbose(
"Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1) "Creating new document %s with text from line %d to %d" % (
nHandle, iStart, iEnd-1
)
) )
theText = "\n".join(theLines[iStart:iEnd]) theText = "\n".join(theLines[iStart:iEnd])
@@ -227,7 +231,9 @@ class GuiDocSplit(QDialog):
spLevel = self.splitLevel.currentData() spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel) self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)) logger.debug(
"Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)
)
lineNo = 0 lineNo = 0
for aLine in theText.splitlines(): for aLine in theText.splitlines():
@@ -44,12 +44,12 @@ from nw.constants import nwAlert, nwQuotes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiConfigEditor(PagedDialog): class GuiPreferences(PagedDialog):
def __init__(self, theParent, theProject): def __init__(self, theParent, theProject):
PagedDialog.__init__(self, theParent) PagedDialog.__init__(self, theParent)
logger.debug("Initialising ConfigEditor ...") logger.debug("Initialising GuiPreferences ...")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theParent = theParent self.theParent = theParent
@@ -74,7 +74,7 @@ class GuiConfigEditor(PagedDialog):
self.show() self.show()
logger.debug("ConfigEditor initialisation complete") logger.debug("GuiPreferences initialisation complete")
return return
@@ -122,7 +122,7 @@ class GuiConfigEditor(PagedDialog):
self.close() self.close()
return return
# END Class GuiConfigEditor # END Class GuiPreferences
class GuiConfigEditGeneralTab(QWidget): class GuiConfigEditGeneralTab(QWidget):
@@ -228,7 +228,7 @@ class GuiConfigEditGeneralTab(QWidget):
## Backup Path ## Backup Path
self.backupPath = self.mainConf.backupPath self.backupPath = self.mainConf.backupPath
self.backupGetPath = QPushButton(self.theTheme.getIcon("folder-open"),"Select Folder") self.backupGetPath = QPushButton("Browse")
self.backupGetPath.clicked.connect(self._backupFolder) self.backupGetPath.clicked.connect(self._backupFolder)
self.backupPathRow = self.mainForm.addRow( self.backupPathRow = self.mainForm.addRow(
"Backup storage location", "Backup storage location",
+3 -1
View File
@@ -362,7 +362,9 @@ class GuiProjectEditStatus(QWidget):
self.listBox.takeItem(iRow) self.listBox.takeItem(iRow)
self.colChanged = True self.colChanged = True
else: else:
self.theParent.makeAlert("Cannot delete status item that is in use.",nwAlert.ERROR) self.theParent.makeAlert(
"Cannot delete status item that is in use.", nwAlert.ERROR
)
return return
def _saveItem(self): def _saveItem(self):
+14 -6
View File
@@ -180,11 +180,15 @@ class GuiSessionLogView(QDialog):
inData = inLine.split() inData = inLine.split()
if len(inData) != 8: if len(inData) != 8:
continue continue
dStart = datetime.strptime("%s %s" % (inData[1],inData[2]), nwConst.tStampFmt) dStart = datetime.strptime(
dEnd = datetime.strptime("%s %s" % (inData[4],inData[5]), nwConst.tStampFmt) "%s %s" % (inData[1],inData[2]), nwConst.tStampFmt
)
dEnd = datetime.strptime(
"%s %s" % (inData[4],inData[5]), nwConst.tStampFmt
)
nWords = int(inData[7]) nWords = int(inData[7])
tDiff = dEnd - dStart tDiff = dEnd - dStart
sDiff = tDiff.total_seconds() sDiff = tDiff.total_seconds()
self.timeTotal += sDiff self.timeTotal += sDiff
if abs(nWords) > 0: if abs(nWords) > 0:
@@ -196,7 +200,9 @@ class GuiSessionLogView(QDialog):
if hideNegative and nWords < 0: if hideNegative and nWords < 0:
continue continue
newItem = QTreeWidgetItem([str(dStart),self._formatTime(sDiff),str(nWords),""]) newItem = QTreeWidgetItem(
[str(dStart), self._formatTime(sDiff), str(nWords), ""]
)
newItem.setTextAlignment(1,Qt.AlignRight) newItem.setTextAlignment(1,Qt.AlignRight)
newItem.setTextAlignment(2,Qt.AlignRight) newItem.setTextAlignment(2,Qt.AlignRight)
@@ -208,7 +214,9 @@ class GuiSessionLogView(QDialog):
self.listBox.addTopLevelItem(newItem) self.listBox.addTopLevelItem(newItem)
except Exception as e: except Exception as e:
self.theParent.makeAlert(["Failed to read session log file.",str(e)], nwAlert.ERROR) self.theParent.makeAlert(
["Failed to read session log file.",str(e)], nwAlert.ERROR
)
return False return False
self.labelFilter.setText(self._formatTime(self.timeFilter)) self.labelFilter.setText(self._formatTime(self.timeFilter))
+13 -4
View File
@@ -405,6 +405,8 @@ class GuiDocEditor(QTextEdit):
def setCursorPosition(self, thePosition): def setCursorPosition(self, thePosition):
"""Move the cursor to a given position in the document. """Move the cursor to a given position in the document.
""" """
if not isinstance(thePosition, int):
return False
if thePosition >= 0: if thePosition >= 0:
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.setPosition(thePosition) theCursor.setPosition(thePosition)
@@ -420,12 +422,13 @@ class GuiDocEditor(QTextEdit):
def setCursorLine(self, theLine): def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document. """Move the cursor to a given line in the document.
""" """
if theLine is None: if not isinstance(theLine, int):
return False return False
if theLine >= 0: if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine) theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock: if theBlock:
self.setCursorPosition(theBlock.position()) self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d" % theLine)
return True return True
## ##
@@ -480,7 +483,9 @@ class GuiDocEditor(QTextEdit):
self.hLight.rehighlight() self.hLight.rehighlight()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
afTime = time() afTime = time()
logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) logger.debug(
"Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))
)
return True return True
@@ -717,7 +722,9 @@ class GuiDocEditor(QTextEdit):
""" """
sinceActive = time()-self.lastEdit sinceActive = time()-self.lastEdit
if sinceActive > 5*self.wcInterval: if sinceActive > 5*self.wcInterval:
logger.debug("Stopping word count timer: no activity last %.1f seconds" % sinceActive) logger.debug(
"Stopping word count timer: no activity last %.1f seconds" % sinceActive
)
self.wcTimer.stop() self.wcTimer.stop()
elif self.wCounter.isRunning(): elif self.wCounter.isRunning():
logger.verbose("Word counter thread is busy") logger.verbose("Word counter thread is busy")
@@ -953,7 +960,9 @@ class GuiDocEditor(QTextEdit):
theText = newText theText = newText
cOffset -= 0 cOffset -= 0
else: else:
logger.error("Unknown or unsupported block format requested: %s" % str(docAction)) logger.error(
"Unknown or unsupported block format requested: %s" % str(docAction)
)
return return
# Replace the block text # Replace the block text
+2 -2
View File
@@ -396,7 +396,7 @@ class GuiDocTree(QTreeWidget):
trItemP.takeChild(tIndex) trItemP.takeChild(tIndex)
del self.theProject.projTree[tHandle] del self.theProject.projTree[tHandle]
else: else:
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR) self.makeAlert("Cannot delete folder. It is not empty.", nwAlert.ERROR)
return False return False
elif nwItemS.itemType == nwItemType.ROOT: elif nwItemS.itemType == nwItemType.ROOT:
@@ -407,7 +407,7 @@ class GuiDocTree(QTreeWidget):
self.theParent.mainMenu.setAvailableRoot() self.theParent.mainMenu.setAvailableRoot()
self.theProject.setProjectChanged(True) self.theProject.setProjectChanged(True)
else: else:
self.makeAlert(["Cannot delete root folder.","It is not empty."], nwAlert.ERROR) self.makeAlert("Cannot delete root folder. It is not empty.", nwAlert.ERROR)
return False return False
return True return True
+50 -29
View File
@@ -28,7 +28,7 @@
import logging import logging
import nw import nw
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QTextBrowser from PyQt5.QtWidgets import QTextBrowser
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
@@ -134,6 +134,7 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent) aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments) aDoc.setPreview(True, self.mainConf.viewComments)
aDoc.setLinkHeaders(True)
aDoc.setText(tHandle) aDoc.setText(tHandle)
aDoc.doAutoReplace() aDoc.doAutoReplace()
aDoc.tokenizeText() aDoc.tokenizeText()
@@ -164,19 +165,17 @@ class GuiDocViewer(QTextBrowser):
index being up to date. index being up to date.
""" """
logger.debug("Loading document from tag '%s'" % theTag) logger.debug("Loading document from tag '%s'" % theTag)
tHandle, onLine, sTitle = self.theParent.theIndex.getTagSource(theTag)
if theTag in self.theParent.theIndex.tagIndex.keys(): if tHandle is None:
theTarget = self.theParent.theIndex.tagIndex[theTag] self.theParent.makeAlert((
"Could not find the reference for tag '%s'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing F9."
) % theTag, nwAlert.ERROR)
return
else: else:
logger.debug("The tag was not found in the index") self.loadText(tHandle)
return False self.navigateTo("#head_%s:%s" % (tHandle, sTitle))
if len(theTarget) != 3:
# Just to make sure the index is not messed up
return False
self.loadText(theTarget[1])
return True return True
def docAction(self, theAction): def docAction(self, theAction):
@@ -200,6 +199,15 @@ class GuiDocViewer(QTextBrowser):
return False return False
return True return True
def navigateTo(self, navLink):
"""Go to a specific #link in the document.
"""
if not isinstance(navLink, str):
return False
if navLink.startswith("#"):
self.setSource(QUrl(navLink))
return True
def updateDocMargins(self): def updateDocMargins(self):
"""Automatically adjust the margins so the text is centred if """Automatically adjust the margins so the text is centred if
Config.textFixedW is enabled or we're in Zen mode. Otherwise, Config.textFixedW is enabled or we're in Zen mode. Otherwise,
@@ -233,6 +241,33 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins() self.updateDocMargins()
return return
##
# Setters
##
def setCursorPosition(self, thePosition):
"""Move the cursor to a given position in the document.
"""
if not isinstance(thePosition, int):
return False
if thePosition >= 0:
theCursor = self.textCursor()
theCursor.setPosition(thePosition)
self.setTextCursor(theCursor)
return True
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d" % theLine)
return True
## ##
# Events # Events
## ##
@@ -262,25 +297,11 @@ class GuiDocViewer(QTextBrowser):
"""Slot for a link in the document being clicked. """Slot for a link in the document being clicked.
""" """
theLink = theURL.url() theLink = theURL.url()
tHandle = None logger.verbose("Clicked link: '%s'" % theLink)
onLine = 0
theTag = ""
if len(theLink) > 0: if len(theLink) > 0:
theBits = theLink.split("=") theBits = theLink.split("=")
if len(theBits) == 2: if len(theBits) == 2:
theTag = theBits[1] self.loadFromTag(theBits[1])
tHandle, onLine = self.theParent.theIndex.getTagSource(theBits[1])
if tHandle is None:
self.theParent.makeAlert((
"Could not find the reference for tag '%s'. It either doesn't exist, or the index "
"is out of date. The index can be updated from the Tools menu.") % theTag,
nwAlert.ERROR
)
return
else:
self.loadText(tHandle)
return return
def _makeStyleSheet(self): def _makeStyleSheet(self):
+7 -4
View File
@@ -107,7 +107,9 @@ class GuiDocViewDetails(QWidget):
for tHandle in theRefs: for tHandle in theRefs:
tItem = self.theProject.projTree[tHandle] tItem = self.theProject.projTree[tHandle]
if tItem is not None: if tItem is not None:
theList.append("<a href='#tag=%s'>%s</a>" % (tHandle,tItem.itemName)) theList.append("<a href='#head_%s:%s'>%s</a>" % (
tHandle, theRefs[tHandle], tItem.itemName
))
self.refList.setText(", ".join(theList)) self.refList.setText(", ".join(theList))
self.refList.adjustSize() self.refList.adjustSize()
@@ -122,9 +124,10 @@ class GuiDocViewDetails(QWidget):
"""Capture the link-click and forward it to the document viewer """Capture the link-click and forward it to the document viewer
class for handling. class for handling.
""" """
if len(theLink) == 18: logger.verbose("Clicked link: '%s'" % theLink)
tHandle = theLink[-13:] if len(theLink) == 27:
self.theParent.viewDocument(tHandle) tHandle = theLink[6:19]
self.theParent.viewDocument(tHandle, theLink)
return return
def _doShowHide(self, chState): def _doShowHide(self, chState):
+6 -2
View File
@@ -249,7 +249,9 @@ class GuiIcons:
try: try:
confParser.read_file(open(themeConf, mode="r", encoding="utf8")) confParser.read_file(open(themeConf, mode="r", encoding="utf8"))
except Exception as e: except Exception as e:
self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR) self.theParent.makeAlert(
["Could not load theme config file.",str(e)], nwAlert.ERROR
)
continue continue
themeName = "" themeName = ""
if confParser.has_section("Main"): if confParser.has_section("Main"):
@@ -295,7 +297,9 @@ class GuiIcons:
# Finally. we check if we have a fallback icon # Finally. we check if we have a fallback icon
if self.mainConf.guiDark: if self.mainConf.guiDark:
fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey) fbackIcon = path.join(
self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey
)
if path.isfile(fbackIcon): if path.isfile(fbackIcon):
logger.verbose("Loading icon '%s' from fallback theme" % iconKey) logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
return QIcon(fbackIcon) return QIcon(fbackIcon)
+5 -1
View File
@@ -137,13 +137,17 @@ class GuiMainStatus(QStatusBar):
self._updateTime() self._updateTime()
return True return True
##
# Setters
##
def setRefTime(self, theTime): def setRefTime(self, theTime):
"""Set the reference time for the status bar clock. """Set the reference time for the status bar clock.
""" """
self.refTime = theTime self.refTime = theTime
return return
def setStatus(self, theMessage, timeOut=10.0): def setStatus(self, theMessage, timeOut=20.0):
"""Set the status bar message to display for 'timeOut' seconds. """Set the status bar message to display for 'timeOut' seconds.
""" """
self.showMessage(theMessage, int(timeOut*1000)) self.showMessage(theMessage, int(timeOut*1000))
+6 -2
View File
@@ -284,7 +284,9 @@ class GuiTheme:
try: try:
confParser.read_file(open(themeConf, mode="r", encoding="utf8")) confParser.read_file(open(themeConf, mode="r", encoding="utf8"))
except Exception as e: except Exception as e:
self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR) self.theParent.makeAlert(
["Could not load theme config file.",str(e)], nwAlert.ERROR
)
continue continue
themeName = "" themeName = ""
if confParser.has_section("Main"): if confParser.has_section("Main"):
@@ -314,7 +316,9 @@ class GuiTheme:
try: try:
confParser.read_file(open(syntaxPath, mode="r", encoding="utf8")) confParser.read_file(open(syntaxPath, mode="r", encoding="utf8"))
except Exception as e: except Exception as e:
self.theParent.makeAlert(["Could not load syntax file.",str(e)],nwAlert.ERROR) self.theParent.makeAlert(
["Could not load syntax file.",str(e)], nwAlert.ERROR
)
return [] return []
syntaxName = "" syntaxName = ""
if confParser.has_section("Main"): if confParser.has_section("Main"):
+25 -19
View File
@@ -42,7 +42,7 @@ from PyQt5.QtWidgets import (
from nw.gui import ( from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectSettings, GuiItemEditor, GuiProjectOutline, GuiPreferences, GuiProjectSettings, GuiItemEditor, GuiProjectOutline,
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel
) )
from nw.core import NWProject, NWDoc, NWIndex from nw.core import NWProject, NWDoc, NWIndex
@@ -485,7 +485,7 @@ class GuiMain(QMainWindow):
self.docEditor.saveText() self.docEditor.saveText()
return True return True
def viewDocument(self, tHandle=None): def viewDocument(self, tHandle=None, navLink=None):
"""Load a document for viewing in the view panel. """Load a document for viewing in the view panel.
""" """
if tHandle is None: if tHandle is None:
@@ -503,13 +503,15 @@ class GuiMain(QMainWindow):
# Make sure main tab is in Editor view # Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitView) self.tabWidget.setCurrentWidget(self.splitView)
if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible(): if self.docViewer.loadText(tHandle):
bPos = self.splitMain.sizes() if not self.viewPane.isVisible():
self.viewPane.setVisible(True) bPos = self.splitMain.sizes()
vPos = [0,0] self.viewPane.setVisible(True)
vPos[0] = int(bPos[1]/2) vPos = [0,0]
vPos[1] = bPos[1]-vPos[0] vPos[0] = int(bPos[1]/2)
self.splitView.setSizes(vPos) vPos[1] = bPos[1]-vPos[0]
self.splitView.setSizes(vPos)
self.docViewer.navigateTo(navLink)
return True return True
@@ -551,7 +553,7 @@ class GuiMain(QMainWindow):
if self.docEditor.theHandle is None: if self.docEditor.theHandle is None:
self.makeAlert( self.makeAlert(
["Please open a document to import the text file into."], "Please open a document to import the text file into.",
nwAlert.ERROR nwAlert.ERROR
) )
return False return False
@@ -666,6 +668,12 @@ class GuiMain(QMainWindow):
theDoc = NWDoc(self.theProject, self) theDoc = NWDoc(self.theProject, self)
for nDone, tItem in enumerate(self.theProject.projTree): for nDone, tItem in enumerate(self.theProject.projTree):
if tItem is not None:
self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName)
else:
self.statusBar.setStatus("Indexing: Unknown item")
if tItem is not None and tItem.itemType == nwItemType.FILE: if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning: %s" % tItem.itemName) logger.verbose("Scanning: %s" % tItem.itemName)
theText = theDoc.openDocument(tItem.itemHandle, showStatus=False) theText = theDoc.openDocument(tItem.itemHandle, showStatus=False)
@@ -681,16 +689,14 @@ class GuiMain(QMainWindow):
self.treeView.propagateCount(tItem.itemHandle, wC) self.treeView.propagateCount(tItem.itemHandle, wC)
self.treeView.projectWordCount() self.treeView.projectWordCount()
self.statusBar.setStatus("Building index: %.2f%%" % (100.0*(nDone + 1)/nItems))
self.docEditor.reloadText()
qApp.restoreOverrideCursor()
tEnd = time() tEnd = time()
self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.docEditor.reloadText()
qApp.restoreOverrideCursor()
if self.mainConf.showGUI: if self.mainConf.showGUI:
self.makeAlert( self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO)
"Project index rebuilt in %.3f seconds." % (tEnd - tStart), nwAlert.INFO
)
return True return True
@@ -735,7 +741,7 @@ class GuiMain(QMainWindow):
def editConfigDialog(self): def editConfigDialog(self):
"""Open the preferences dialog. """Open the preferences dialog.
""" """
dlgConf = GuiConfigEditor(self, self.theProject) dlgConf = GuiPreferences(self, self.theProject)
if dlgConf.exec_() == QDialog.Accepted: if dlgConf.exec_() == QDialog.Accepted:
logger.debug("Applying new preferences") logger.debug("Applying new preferences")
self.initMain() self.initMain()
@@ -776,7 +782,7 @@ class GuiMain(QMainWindow):
0 = info, 1 = warning, and 2 = error. 0 = info, 1 = warning, and 2 = error.
""" """
if isinstance(theMessage, list): if isinstance(theMessage, list):
popMsg = " ".join(theMessage) popMsg = "<br>".join(theMessage)
logMsg = theMessage logMsg = theMessage
else: else:
popMsg = theMessage popMsg = theMessage
+82
View File
@@ -0,0 +1,82 @@
[
[
"content/14298de4d9524.nwd",
"CHARACTER",
"John Smith"
],
[
"content/53b69b83cdafc.nwd",
"NOVEL",
"Title Page"
],
[
"content/5eaea4e8cdee8.nwd",
"WORLD",
"Mars"
],
[
"content/636b6aa9b697b.nwd",
"NOVEL",
"Making a Scene"
],
[
"content/6a2d6d5f4f401.nwd",
"NOVEL",
"Chapter One"
],
[
"content/88706ddc78b1b.nwd",
"NOVEL",
"Chapter Two"
],
[
"content/96b68994dfa3d.nwd",
"NOVEL",
"A Note on Structure"
],
[
"content/974e400180a99.nwd",
"NOVEL",
"Page"
],
[
"content/ae7339df26ded.nwd",
"NOVEL",
"We Found John!"
],
[
"content/b3e74dbc1f584.nwd",
"WORLD",
"Earth"
],
[
"content/b8136a5a774a0.nwd",
"NOVEL",
"Delete Me!"
],
[
"content/ba8a28a246524.nwd",
"NOVEL",
"Interlude"
],
[
"content/bb2c23b3c42cc.nwd",
"CHARACTER",
"Jane Smith"
],
[
"content/bc0cbd2a407f3.nwd",
"NOVEL",
"Another Scene"
],
[
"content/edca4be2fcaf8.nwd",
"NOVEL",
"Part One"
],
[
"content/f1471bef9f2ae.nwd",
"WORLD",
"Space"
]
]
+23
View File
@@ -0,0 +1,23 @@
Table of Contents
===================
File Name Class Document Label
--------------------------------------------------------------------------------
content/14298de4d9524.nwd CHARACTER John Smith
content/53b69b83cdafc.nwd NOVEL Title Page
content/5eaea4e8cdee8.nwd WORLD Mars
content/636b6aa9b697b.nwd NOVEL Making a Scene
content/6a2d6d5f4f401.nwd NOVEL Chapter One
content/88706ddc78b1b.nwd NOVEL Chapter Two
content/96b68994dfa3d.nwd NOVEL A Note on Structure
content/974e400180a99.nwd NOVEL Page
content/ae7339df26ded.nwd NOVEL We Found John!
content/b3e74dbc1f584.nwd WORLD Earth
content/b8136a5a774a0.nwd NOVEL Delete Me!
content/ba8a28a246524.nwd NOVEL Interlude
content/bb2c23b3c42cc.nwd CHARACTER Jane Smith
content/bc0cbd2a407f3.nwd NOVEL Another Scene
content/edca4be2fcaf8.nwd NOVEL Part One
content/f1471bef9f2ae.nwd WORLD Space
+4 -20
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="186" autoCount="28" timeStamp="2020-05-28 09:59:15"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="202" autoCount="29" timeStamp="2020-05-30 00:19:45" editTime="35">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
@@ -11,7 +11,7 @@
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited> <lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>ba8a28a246524</lastViewed> <lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>914</lastWordCount> <lastWordCount>914</lastWordCount>
<autoReplace> <autoReplace>
<A>B</A> <A>B</A>
@@ -57,7 +57,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>Started</status> <status>Started</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>TITLE</layout> <layout>TITLE</layout>
<charCount>72</charCount> <charCount>72</charCount>
@@ -70,10 +69,9 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>PAGE</layout> <layout>PAGE</layout>
<charCount>208</charCount> <charCount>210</charCount>
<wordCount>40</wordCount> <wordCount>40</wordCount>
<paraCount>2</paraCount> <paraCount>2</paraCount>
<cursorPos>213</cursorPos> <cursorPos>213</cursorPos>
@@ -83,7 +81,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>PARTITION</layout> <layout>PARTITION</layout>
<charCount>23</charCount> <charCount>23</charCount>
@@ -103,7 +100,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>Notes</status> <status>Notes</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>CHAPTER</layout> <layout>CHAPTER</layout>
<charCount>12</charCount> <charCount>12</charCount>
@@ -116,20 +112,18 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>1st Draft</status> <status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>1199</charCount> <charCount>1199</charCount>
<wordCount>216</wordCount> <wordCount>216</wordCount>
<paraCount>7</paraCount> <paraCount>7</paraCount>
<cursorPos>527</cursorPos> <cursorPos>1266</cursorPos>
</item> </item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a"> <item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name> <name>Another Scene</name>
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>1st Draft</status> <status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>476</charCount> <charCount>476</charCount>
@@ -142,7 +136,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>Finished</status> <status>Finished</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>UNNUMBERED</layout> <layout>UNNUMBERED</layout>
<charCount>633</charCount> <charCount>633</charCount>
@@ -155,7 +148,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>2nd Draft</status> <status>2nd Draft</status>
<expanded>False</expanded>
<exported>False</exported> <exported>False</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>1692</charCount> <charCount>1692</charCount>
@@ -168,7 +160,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>1st Draft</status> <status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>CHAPTER</layout> <layout>CHAPTER</layout>
<charCount>139</charCount> <charCount>139</charCount>
@@ -181,7 +172,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>1st Draft</status> <status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>189</charCount> <charCount>189</charCount>
@@ -208,7 +198,6 @@
<type>FILE</type> <type>FILE</type>
<class>CHARACTER</class> <class>CHARACTER</class>
<status>Minor</status> <status>Minor</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>49</charCount> <charCount>49</charCount>
@@ -221,7 +210,6 @@
<type>FILE</type> <type>FILE</type>
<class>CHARACTER</class> <class>CHARACTER</class>
<status>Major</status> <status>Major</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>55</charCount> <charCount>55</charCount>
@@ -241,7 +229,6 @@
<type>FILE</type> <type>FILE</type>
<class>WORLD</class> <class>WORLD</class>
<status>Main</status> <status>Main</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>76</charCount> <charCount>76</charCount>
@@ -254,7 +241,6 @@
<type>FILE</type> <type>FILE</type>
<class>WORLD</class> <class>WORLD</class>
<status>Minor</status> <status>Minor</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>115</charCount> <charCount>115</charCount>
@@ -267,7 +253,6 @@
<type>FILE</type> <type>FILE</type>
<class>WORLD</class> <class>WORLD</class>
<status>Major</status> <status>Major</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>28</charCount> <charCount>28</charCount>
@@ -287,7 +272,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+5 -2
View File
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup( setuptools.setup(
name = "novelWriter", name = "novelWriter",
version = "0.6.3", version = "0.7.0rc1",
author = "Veronica Berglyd Olsen", author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net", author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels", description = "A markdown-like document editor for writing novels",
@@ -27,7 +27,10 @@ setuptools.setup(
"Source Code": "https://github.com/vkbo/novelWriter", "Source Code": "https://github.com/vkbo/novelWriter",
}, },
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Operating System :: OS Independent", "Operating System :: OS Independent",
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-27 18:25:48"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:05:28">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -14,7 +14,7 @@
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -55,7 +55,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+2 -6
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-27 18:27:20"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="4" autoCount="0" timeStamp="2020-05-29 23:06:02">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -14,7 +14,7 @@
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -55,7 +55,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>331</charCount> <charCount>331</charCount>
@@ -75,7 +74,6 @@
<type>FILE</type> <type>FILE</type>
<class>CHARACTER</class> <class>CHARACTER</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>34</charCount> <charCount>34</charCount>
@@ -95,7 +93,6 @@
<type>FILE</type> <type>FILE</type>
<class>PLOT</class> <class>PLOT</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>48</charCount> <charCount>48</charCount>
@@ -115,7 +112,6 @@
<type>FILE</type> <type>FILE</type>
<class>WORLD</class> <class>WORLD</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>51</charCount> <charCount>51</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-10 23:19:00"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:07:05">
<project> <project>
<name>Project Name</name> <name>Project Name</name>
<title>Project Title</title> <title>Project Title</title>
@@ -18,7 +18,7 @@
</autoReplace> </autoReplace>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -59,7 +59,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-27 18:30:26"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="2" autoCount="0" timeStamp="2020-05-29 23:07:37">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -14,7 +14,7 @@
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -55,7 +55,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>Note</status> <status>Note</status>
<expanded>False</expanded>
<exported>False</exported> <exported>False</exported>
<layout>PAGE</layout> <layout>PAGE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="1" autoCount="0" timeStamp="2020-05-27 18:23:46"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="1" autoCount="0" timeStamp="2020-05-29 23:03:58">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -14,7 +14,7 @@
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -76,7 +76,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+2 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-27 18:24:35"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="4" autoCount="0" timeStamp="2020-05-29 23:04:31">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -14,7 +14,7 @@
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -76,7 +76,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+2 -5
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.1" hexVersion="0x000601f0" fileVersion="1.0" saveCount="5" autoCount="0" timeStamp="2020-05-27 18:24:56"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="5" autoCount="0" timeStamp="2020-05-29 23:04:50">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -14,7 +14,7 @@
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter> <chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>* * *</scene> <scene>* * *</scene>
<section></section> <section></section>
@@ -76,7 +76,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
@@ -117,7 +116,6 @@
<type>FILE</type> <type>FILE</type>
<class>NOVEL</class> <class>NOVEL</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>0</charCount> <charCount>0</charCount>
@@ -130,7 +128,6 @@
<type>FILE</type> <type>FILE</type>
<class>CHARACTER</class> <class>CHARACTER</class>
<status>New</status> <status>New</status>
<expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>0</charCount> <charCount>0</charCount>
+8 -8
View File
@@ -237,14 +237,14 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
# Check the files # Check the files
refFile = path.join(nwTempGUI,"nwProject.nwx") refFile = path.join(nwTempGUI,"nwProject.nwx")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2]) assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2])
refFile = path.join(nwTempGUI,"data_0","e17daca5f3e1_main.nwd") refFile = path.join(nwTempGUI,"content","0e17daca5f3e1.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_e17daca5f3e1_main.nwd")) assert cmpFiles(refFile, path.join(nwRef,"gui","1_0e17daca5f3e1.nwd"))
refFile = path.join(nwTempGUI,"data_9","8010bd9270f9_main.nwd") refFile = path.join(nwTempGUI,"content","98010bd9270f9.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_8010bd9270f9_main.nwd")) assert cmpFiles(refFile, path.join(nwRef,"gui","1_98010bd9270f9.nwd"))
refFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd") refFile = path.join(nwTempGUI,"content","31489056e0916.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) assert cmpFiles(refFile, path.join(nwRef,"gui","1_31489056e0916.nwd"))
refFile = path.join(nwTempGUI,"data_1","a6562590ef19_main.nwd") refFile = path.join(nwTempGUI,"content","1a6562590ef19.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_a6562590ef19_main.nwd")) assert cmpFiles(refFile, path.join(nwRef,"gui","1_1a6562590ef19.nwd"))
nwGUI.closeMain() nwGUI.closeMain()
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
+3 -3
View File
@@ -155,7 +155,7 @@ def testIndexCheckThese(nwTempProj):
"# Hello World!\n" "# Hello World!\n"
"@pov: Jane" "@pov: Jane"
)) ))
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}" assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]" assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
@@ -195,7 +195,7 @@ def testIndexMeta(nwTempProj):
"\n" "\n"
"Well, not really.\n" "Well, not really.\n"
)) ))
assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}" assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
# The novel structure should contain the pointer to the novel file header # The novel structure should contain the pointer to the novel file header
@@ -214,6 +214,6 @@ def testIndexMeta(nwTempProj):
# The character file should have a record of the reference from the novel file # The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle) theRefs = theIndex.getBackReferenceList(cHandle)
assert str(theRefs) == "{'41cfc0d1f2d12': 3}" assert str(theRefs) == "{'41cfc0d1f2d12': 'T000001'}"
assert theProject.closeProject() assert theProject.closeProject()