Add hash and dates to document meta data (#1509)
This commit is contained in:
@@ -25,7 +25,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
@@ -488,25 +487,6 @@ def makeFileNameSafe(text: str) -> str:
|
|||||||
return "".join(c for c in text if c.isalnum() or c in allowed)
|
return "".join(c for c in text if c.isalnum() or c in allowed)
|
||||||
|
|
||||||
|
|
||||||
def sha256sum(path: str | Path) -> str | None:
|
|
||||||
"""Make a shasum of a file using a buffer.
|
|
||||||
Based on: https://stackoverflow.com/a/44873382/5825851
|
|
||||||
"""
|
|
||||||
digest = hashlib.sha256()
|
|
||||||
bData = bytearray(65536)
|
|
||||||
mData = memoryview(bData)
|
|
||||||
try:
|
|
||||||
with open(path, mode="rb", buffering=0) as inFile:
|
|
||||||
for n in iter(lambda: inFile.readinto(mData), 0):
|
|
||||||
digest.update(mData[:n])
|
|
||||||
except Exception:
|
|
||||||
logger.error("Could not create sha256sum of: %s", path)
|
|
||||||
logException()
|
|
||||||
return None
|
|
||||||
|
|
||||||
return digest.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
# Other Functions
|
# Other Functions
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
|
|||||||
@@ -23,15 +23,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from time import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from novelwriter.core.item import NWItem
|
|
||||||
|
|
||||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||||
from novelwriter.error import formatException
|
from novelwriter.error import formatException
|
||||||
from novelwriter.common import isHandle, sha256sum
|
from novelwriter.common import formatTimeStamp, isHandle
|
||||||
|
from novelwriter.core.item import NWItem
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
@@ -50,30 +52,38 @@ class NWDocument:
|
|||||||
|
|
||||||
def __init__(self, project: NWProject, tHandle: str | None) -> None:
|
def __init__(self, project: NWProject, tHandle: str | None) -> None:
|
||||||
|
|
||||||
self._project = project
|
self._project = project
|
||||||
|
|
||||||
# Internal Variables
|
self._item = None # The currently open item
|
||||||
self._theItem = None # The currently open item
|
self._handle = None # The handle of the currently open item
|
||||||
self._docHandle = None # The handle of the currently open item
|
self._fileLoc = None # The file location of the currently open item
|
||||||
self._fileLoc = None # The file location of the currently open item
|
self._docMeta = {} # The meta data of the currently open item
|
||||||
self._docMeta = {} # The meta data of the currently open item
|
self._docError = "" # The latest encountered IO error
|
||||||
self._docError = "" # The latest encountered IO error
|
self._lastHash = "" # The last known SHA hash
|
||||||
self._prevHash = None # Previous sha256sum of the document file
|
self._hashError = False # Hash mismatch on last write attempt
|
||||||
self._currHash = None # Latest sha256sum of the document file
|
|
||||||
|
|
||||||
if isHandle(tHandle):
|
if isHandle(tHandle):
|
||||||
self._docHandle = tHandle
|
self._handle = tHandle
|
||||||
|
|
||||||
if self._docHandle is not None:
|
if self._handle is not None:
|
||||||
self._theItem = self._project.tree[tHandle]
|
self._item = self._project.tree[tHandle]
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<NWDocument handle={self._docHandle}>"
|
return f"<NWDocument handle={self._handle}>"
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
return self._docHandle is not None and bool(self._theItem)
|
return self._handle is not None and self._item is not None
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hashError(self) -> bool:
|
||||||
|
"""Check if the file hash has changed outside of novelWriter."""
|
||||||
|
return self._hashError
|
||||||
|
|
||||||
##
|
##
|
||||||
# Class Methods
|
# Class Methods
|
||||||
@@ -81,7 +91,7 @@ class NWDocument:
|
|||||||
|
|
||||||
def fileExists(self) -> bool:
|
def fileExists(self) -> bool:
|
||||||
"""Check if the document file exists."""
|
"""Check if the document file exists."""
|
||||||
if self._docHandle is None:
|
if self._handle is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
contentPath = self._project.storage.contentPath
|
contentPath = self._project.storage.contentPath
|
||||||
@@ -89,7 +99,7 @@ class NWDocument:
|
|||||||
logger.error("No content path set")
|
logger.error("No content path set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return (contentPath / f"{self._docHandle}.nwd").is_file()
|
return (contentPath / f"{self._handle}.nwd").is_file()
|
||||||
|
|
||||||
def readDocument(self, isOrphan: bool = False) -> str | None:
|
def readDocument(self, isOrphan: bool = False) -> str | None:
|
||||||
"""Read the document specified by the handle set in the
|
"""Read the document specified by the handle set in the
|
||||||
@@ -98,11 +108,11 @@ class NWDocument:
|
|||||||
empty string. If something went wrong, return None.
|
empty string. If something went wrong, return None.
|
||||||
"""
|
"""
|
||||||
self._docError = ""
|
self._docError = ""
|
||||||
if not isinstance(self._docHandle, str):
|
if not isinstance(self._handle, str):
|
||||||
logger.error("No document handle set")
|
logger.error("No document handle set")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._theItem is None and not isOrphan:
|
if self._item is None and not isOrphan:
|
||||||
logger.error("Unknown novelWriter document")
|
logger.error("Unknown novelWriter document")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -111,31 +121,30 @@ class NWDocument:
|
|||||||
logger.error("No content path set")
|
logger.error("No content path set")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
docFile = f"{self._docHandle}.nwd"
|
docFile = f"{self._handle}.nwd"
|
||||||
logger.debug("Opening document: %s", docFile)
|
logger.debug("Opening document: %s", docFile)
|
||||||
|
|
||||||
docPath = contentPath / docFile
|
docPath = contentPath / docFile
|
||||||
self._fileLoc = docPath
|
self._fileLoc = docPath
|
||||||
|
|
||||||
theText = ""
|
text = ""
|
||||||
self._docMeta = {}
|
self._docMeta = {}
|
||||||
self._prevHash = None
|
self._lastHash = ""
|
||||||
|
|
||||||
if docPath.exists():
|
if docPath.exists():
|
||||||
self._prevHash = sha256sum(docPath)
|
|
||||||
try:
|
try:
|
||||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||||
# Check the first <= 10 lines for metadata
|
# Check the first <= 10 lines for metadata
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
inLine = inFile.readline()
|
line = inFile.readline()
|
||||||
if inLine.startswith(r"%%~"):
|
if line.startswith(r"%%~"):
|
||||||
self._parseMeta(inLine)
|
self._parseMeta(line)
|
||||||
else:
|
else:
|
||||||
theText = inLine
|
text = line
|
||||||
break
|
break
|
||||||
|
|
||||||
# Load the rest of the file
|
# Load the rest of the file
|
||||||
theText += inFile.read()
|
text += inFile.read()
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._docError = formatException(exc)
|
self._docError = formatException(exc)
|
||||||
@@ -143,19 +152,20 @@ class NWDocument:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# The document file does not exist, so we assume it's a new
|
# The document file does not exist, so we assume it's a new
|
||||||
# document and initialise an empty text string.
|
# document and return an empty text string.
|
||||||
logger.debug("The requested document does not exist")
|
logger.debug("The requested document does not exist")
|
||||||
return ""
|
|
||||||
|
|
||||||
return theText
|
self._lastHash = hashlib.sha1(text.encode()).hexdigest()
|
||||||
|
|
||||||
def writeDocument(self, docText: str, forceWrite: bool = False) -> bool:
|
return text
|
||||||
|
|
||||||
|
def writeDocument(self, text: str, forceWrite: bool = False) -> bool:
|
||||||
"""Write the document specified by the handle attribute. Handle
|
"""Write the document specified by the handle attribute. Handle
|
||||||
any IO errors in the process Returns True if successful, False
|
any IO errors in the process Returns True if successful, False
|
||||||
if not.
|
if not.
|
||||||
"""
|
"""
|
||||||
self._docError = ""
|
self._docError = ""
|
||||||
if not isinstance(self._docHandle, str):
|
if not isinstance(self._handle, str):
|
||||||
logger.error("No document handle set")
|
logger.error("No document handle set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -164,32 +174,45 @@ class NWDocument:
|
|||||||
logger.error("No content path set")
|
logger.error("No content path set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
docFile = f"{self._docHandle}.nwd"
|
docFile = f"{self._handle}.nwd"
|
||||||
logger.debug("Saving document: %s", docFile)
|
logger.debug("Saving document: %s", docFile)
|
||||||
|
|
||||||
docPath = contentPath / docFile
|
docPath = contentPath / docFile
|
||||||
docTemp = docPath.with_suffix(".tmp")
|
docTemp = docPath.with_suffix(".tmp")
|
||||||
|
|
||||||
if self._prevHash is not None and not forceWrite:
|
# Re-read the document on disk to check if it has changed
|
||||||
self._currHash = sha256sum(docPath)
|
prevHash = self._lastHash
|
||||||
if self._currHash is not None and self._currHash != self._prevHash:
|
self.readDocument()
|
||||||
logger.error("File has been altered on disk since opened")
|
if prevHash and self._lastHash != prevHash and not forceWrite:
|
||||||
return False
|
logger.error("File has been altered on disk since opened")
|
||||||
|
self._hashError = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
currTime = formatTimeStamp(time())
|
||||||
|
writeHash = hashlib.sha1(text.encode()).hexdigest()
|
||||||
|
createdDate = self._docMeta.get("created", "Unknown")
|
||||||
|
updatedDate = self._docMeta.get("updated", "Unknown")
|
||||||
|
if writeHash != self._lastHash:
|
||||||
|
updatedDate = currTime
|
||||||
|
if not docPath.is_file():
|
||||||
|
createdDate = currTime
|
||||||
|
updatedDate = currTime
|
||||||
|
|
||||||
# DocMeta Line
|
# DocMeta Line
|
||||||
if self._theItem is None:
|
docMeta = ""
|
||||||
docMeta = ""
|
if self._item:
|
||||||
else:
|
|
||||||
docMeta = (
|
docMeta = (
|
||||||
f"%%~name: {self._theItem.itemName}\n"
|
f"%%~name: {self._item.itemName}\n"
|
||||||
f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n"
|
f"%%~path: {self._item.itemParent}/{self._item.itemHandle}\n"
|
||||||
f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n"
|
f"%%~kind: {self._item.itemClass.name}/{self._item.itemLayout.name}\n"
|
||||||
|
f"%%~hash: {writeHash}\n"
|
||||||
|
f"%%~date: {createdDate}/{updatedDate}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(docTemp, mode="w", encoding="utf-8") as outFile:
|
with open(docTemp, mode="w", encoding="utf-8") as outFile:
|
||||||
outFile.write(docMeta)
|
outFile.write(docMeta)
|
||||||
outFile.write(docText)
|
outFile.write(text)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._docError = formatException(exc)
|
self._docError = formatException(exc)
|
||||||
return False
|
return False
|
||||||
@@ -202,8 +225,8 @@ class NWDocument:
|
|||||||
self._docError = formatException(exc)
|
self._docError = formatException(exc)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._prevHash = sha256sum(docPath)
|
self._lastHash = writeHash
|
||||||
self._currHash = self._prevHash
|
self._hashError = False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -212,7 +235,7 @@ class NWDocument:
|
|||||||
from the project data folder.
|
from the project data folder.
|
||||||
"""
|
"""
|
||||||
self._docError = ""
|
self._docError = ""
|
||||||
if not isinstance(self._docHandle, str):
|
if not isinstance(self._handle, str):
|
||||||
logger.error("No document handle set")
|
logger.error("No document handle set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -221,7 +244,7 @@ class NWDocument:
|
|||||||
logger.error("No content path set")
|
logger.error("No content path set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
docPath = contentPath / f"{self._docHandle}.nwd"
|
docPath = contentPath / f"{self._handle}.nwd"
|
||||||
docTemp = docPath.with_suffix(".tmp")
|
docTemp = docPath.with_suffix(".tmp")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -247,7 +270,7 @@ class NWDocument:
|
|||||||
|
|
||||||
def getCurrentItem(self) -> NWItem | None:
|
def getCurrentItem(self) -> NWItem | None:
|
||||||
"""Return a pointer to the currently open NWItem."""
|
"""Return a pointer to the currently open NWItem."""
|
||||||
return self._theItem
|
return self._item
|
||||||
|
|
||||||
def getMeta(self) -> tuple[str, str | None, nwItemClass | None, nwItemLayout | None]:
|
def getMeta(self) -> tuple[str, str | None, nwItemClass | None, nwItemLayout | None]:
|
||||||
"""Parse the document meta tag and return the name, parent,
|
"""Parse the document meta tag and return the name, parent,
|
||||||
@@ -293,8 +316,18 @@ class NWDocument:
|
|||||||
if metaBits[1] in nwItemLayout.__members__:
|
if metaBits[1] in nwItemLayout.__members__:
|
||||||
self._docMeta["layout"] = nwItemLayout[metaBits[1]]
|
self._docMeta["layout"] = nwItemLayout[metaBits[1]]
|
||||||
|
|
||||||
|
elif metaLine.startswith("%%~hash:"):
|
||||||
|
self._docMeta["hash"] = metaLine[8:].strip()
|
||||||
|
|
||||||
|
elif metaLine.startswith("%%~date:"):
|
||||||
|
metaVal = metaLine[8:].strip()
|
||||||
|
metaBits = metaVal.split("/")
|
||||||
|
if len(metaBits) == 2:
|
||||||
|
self._docMeta["created"] = metaBits[0].strip()
|
||||||
|
self._docMeta["updated"] = metaBits[1].strip()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Ignoring meta data: '%s'", metaLine.strip())
|
logger.debug("Unknown meta data: '%s'", metaLine.strip())
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ class GuiDocEditor(QTextEdit):
|
|||||||
self.saveCursorPosition()
|
self.saveCursorPosition()
|
||||||
if not self._nwDocument.writeDocument(docText):
|
if not self._nwDocument.writeDocument(docText):
|
||||||
saveOk = False
|
saveOk = False
|
||||||
if self._nwDocument._currHash != self._nwDocument._prevHash:
|
if self._nwDocument.hashError:
|
||||||
msgYes = SHARED.question(self.tr(
|
msgYes = SHARED.question(self.tr(
|
||||||
"This document has been changed outside of novelWriter "
|
"This document has been changed outside of novelWriter "
|
||||||
"while it was open. Overwrite the file on disk?"
|
"while it was open. Overwrite the file on disk?"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: John Smith
|
%%~name: John Smith
|
||||||
%%~path: f7e2d9f330615/14298de4d9524
|
%%~path: f7e2d9f330615/14298de4d9524
|
||||||
%%~kind: CHARACTER/NOTE
|
%%~kind: CHARACTER/NOTE
|
||||||
|
%%~hash: 259eff30f10e101cf93e27e8764e851d356d54b8
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:03
|
||||||
# John Smith
|
# John Smith
|
||||||
|
|
||||||
@tag: John
|
@tag: John
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Title Page
|
%%~name: Title Page
|
||||||
%%~path: 7031beac91f75/53b69b83cdafc
|
%%~path: 7031beac91f75/53b69b83cdafc
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: c5dc35d18ecb074a9e41a1410d1bff8021cf0a5b
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:52
|
||||||
#! My Novel
|
#! My Novel
|
||||||
|
|
||||||
>> **By Jane Smith** <<
|
>> **By Jane Smith** <<
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Mars
|
%%~name: Mars
|
||||||
%%~path: 15c4492bd5107/5eaea4e8cdee8
|
%%~path: 15c4492bd5107/5eaea4e8cdee8
|
||||||
%%~kind: WORLD/NOTE
|
%%~kind: WORLD/NOTE
|
||||||
|
%%~hash: 0838371bc03e31f503fd12a8617d94ef36e5f7c7
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:07
|
||||||
# Mars
|
# Mars
|
||||||
|
|
||||||
@tag: Mars
|
@tag: Mars
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Making a Scene
|
%%~name: Making a Scene
|
||||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63
|
||||||
|
%%~date: Unknown/2023-08-25 16:56:11
|
||||||
### Making a Scene
|
### Making a Scene
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Chapter One
|
%%~name: Chapter One
|
||||||
%%~path: 7031beac91f75/6a2d6d5f4f401
|
%%~path: 7031beac91f75/6a2d6d5f4f401
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 89687756c204742d17f1d37a0ba7fa3aadaf00f2
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:56
|
||||||
## So it Begins
|
## So it Begins
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Chapter Two
|
%%~name: Chapter Two
|
||||||
%%~path: 7031beac91f75/88706ddc78b1b
|
%%~path: 7031beac91f75/88706ddc78b1b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: ca68d1134e1b7870b7bf0fcbd8c0d67384970717
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:00
|
||||||
## Where has John Gone?
|
## Where has John Gone?
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Old File
|
%%~name: Old File
|
||||||
%%~path: ae9bf3c3ea159/8a5deb88c0e97
|
%%~path: ae9bf3c3ea159/8a5deb88c0e97
|
||||||
%%~kind: ARCHIVE/DOCUMENT
|
%%~kind: ARCHIVE/DOCUMENT
|
||||||
|
%%~hash: de9f8f39428e9d127504f47c554fc3e701b0b773
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:08
|
||||||
### Discarded Scene
|
### Discarded Scene
|
||||||
|
|
||||||
If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away.
|
If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: A Note on Structure
|
%%~name: A Note on Structure
|
||||||
%%~path: 7031beac91f75/96b68994dfa3d
|
%%~path: 7031beac91f75/96b68994dfa3d
|
||||||
%%~kind: NOVEL/NOTE
|
%%~kind: NOVEL/NOTE
|
||||||
|
%%~hash: f4f88fa2d824ed4e92223bd6744979cd3f9f45e7
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:59
|
||||||
# A Note on Structure
|
# A Note on Structure
|
||||||
|
|
||||||
This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project.
|
This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Page
|
%%~name: Page
|
||||||
%%~path: 7031beac91f75/974e400180a99
|
%%~path: 7031beac91f75/974e400180a99
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:54
|
||||||
[NEW PAGE]
|
[NEW PAGE]
|
||||||
[VSPACE:2]
|
[VSPACE:2]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Chapter One
|
%%~name: Chapter One
|
||||||
%%~path: e5e47ebf63b1c/a520879ca0b45
|
%%~path: e5e47ebf63b1c/a520879ca0b45
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 2285256b970dbb3ccf833d9275cd5f8935b6aff1
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:02
|
||||||
## Chapter One
|
## Chapter One
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: We Found John!
|
%%~name: We Found John!
|
||||||
%%~path: 88706ddc78b1b/ae7339df26ded
|
%%~path: 88706ddc78b1b/ae7339df26ded
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 1da9cf71a57d11c5d94b021a2c3e6a5b8df67f14
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:01
|
||||||
### We Found John!
|
### We Found John!
|
||||||
|
|
||||||
@pov: John
|
@pov: John
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Earth
|
%%~name: Earth
|
||||||
%%~path: 15c4492bd5107/b3e74dbc1f584
|
%%~path: 15c4492bd5107/b3e74dbc1f584
|
||||||
%%~kind: WORLD/NOTE
|
%%~kind: WORLD/NOTE
|
||||||
|
%%~hash: 87f520b4e80af8505af1b868ab25ec6d51b7ff48
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:05
|
||||||
# Earth
|
# Earth
|
||||||
|
|
||||||
@tag: Earth
|
@tag: Earth
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Delete Me!
|
%%~name: Delete Me!
|
||||||
%%~path: 98acd8c76c93a/b8136a5a774a0
|
%%~path: 98acd8c76c93a/b8136a5a774a0
|
||||||
%%~kind: TRASH/DOCUMENT
|
%%~kind: TRASH/DOCUMENT
|
||||||
|
%%~hash: e1eabd80260e03595c8c0a1f82d813d0ae33c273
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:09
|
||||||
### Delete Me!
|
### Delete Me!
|
||||||
|
|
||||||
This scene is trash.
|
This scene is trash.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Interlude
|
%%~name: Interlude
|
||||||
%%~path: 7031beac91f75/ba8a28a246524
|
%%~path: 7031beac91f75/ba8a28a246524
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: df8904011eda90c7b694bed725dc774bf631b348
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:58
|
||||||
##! Interlude
|
##! Interlude
|
||||||
|
|
||||||
% Notice that this document has a title with a ‘!’ in it in addition to the two hash symbols. This means it is an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue.
|
% Notice that this document has a title with a ‘!’ in it in addition to the two hash symbols. This means it is an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Title Page
|
%%~name: Title Page
|
||||||
%%~path: e5e47ebf63b1c/bacb7059e3083
|
%%~path: e5e47ebf63b1c/bacb7059e3083
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 16b55dd0f8dc6bbba266384930a87ec211d471df
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:01
|
||||||
#! Sequel Novel
|
#! Sequel Novel
|
||||||
|
|
||||||
>> **By Jane Doh** <<
|
>> **By Jane Doh** <<
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Jane Smith
|
%%~name: Jane Smith
|
||||||
%%~path: f7e2d9f330615/bb2c23b3c42cc
|
%%~path: f7e2d9f330615/bb2c23b3c42cc
|
||||||
%%~kind: CHARACTER/NOTE
|
%%~kind: CHARACTER/NOTE
|
||||||
|
%%~hash: a69e4ca6ceede2536c8d364f88e6b62ef0ec36c3
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:04
|
||||||
# Jane Smith
|
# Jane Smith
|
||||||
|
|
||||||
@tag: Jane
|
@tag: Jane
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Another Scene
|
%%~name: Another Scene
|
||||||
%%~path: 6a2d6d5f4f401/bc0cbd2a407f3
|
%%~path: 6a2d6d5f4f401/bc0cbd2a407f3
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: b3f9e4b097f7041c77489876577014c435a8be0c
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:57
|
||||||
### Another Scene
|
### Another Scene
|
||||||
|
|
||||||
@pov: John
|
@pov: John
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Part One
|
%%~name: Part One
|
||||||
%%~path: 7031beac91f75/edca4be2fcaf8
|
%%~path: 7031beac91f75/edca4be2fcaf8
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 9dc53a9bcc457d18b775adf7be38ac89df3408ca
|
||||||
|
%%~date: Unknown/2023-08-25 16:51:54
|
||||||
# Part One
|
# Part One
|
||||||
|
|
||||||
>> In the beginning … <<
|
>> In the beginning … <<
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Space
|
%%~name: Space
|
||||||
%%~path: 15c4492bd5107/f1471bef9f2ae
|
%%~path: 15c4492bd5107/f1471bef9f2ae
|
||||||
%%~kind: WORLD/NOTE
|
%%~kind: WORLD/NOTE
|
||||||
|
%%~hash: bc139a020239c9c0d936aa9d0bcc3644ae2f370c
|
||||||
|
%%~date: Unknown/2023-08-25 16:52:06
|
||||||
# Space
|
# Space
|
||||||
|
|
||||||
@tag: Space
|
@tag: Space
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-08-08 22:19:23">
|
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-08-25 16:56:30">
|
||||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1517" autoCount="237" editTime="75241">
|
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1518" autoCount="237" editTime="75281">
|
||||||
<name>Sample Project</name>
|
<name>Sample Project</name>
|
||||||
<title>Sample Project</title>
|
<title>Sample Project</title>
|
||||||
<author>Jane Smith</author>
|
<author>Jane Smith</author>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="748" />
|
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="66" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
|
|||||||
+1
-2
@@ -226,8 +226,7 @@ def prjLipsum():
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def ipsumText():
|
def ipsumText():
|
||||||
"""Return five paragraphs of Lorem Ipsum text.
|
"""Return five paragraphs of Lorem Ipsum text."""
|
||||||
"""
|
|
||||||
thatIpsum = [(
|
thatIpsum = [(
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum co"
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum co"
|
||||||
"mmodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, e"
|
"mmodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, e"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Ancient Europe
|
%%~name: Ancient Europe
|
||||||
%%~path: 60bdf227455cc/04468803b92e1
|
%%~path: 60bdf227455cc/04468803b92e1
|
||||||
%%~kind: WORLD/NOTE
|
%%~kind: WORLD/NOTE
|
||||||
|
%%~hash: b4318c2de40a1fc4055d18f8d02696b9cf171ea2
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
# Ancient Europe
|
# Ancient Europe
|
||||||
|
|
||||||
@tag: Europe
|
@tag: Europe
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Main
|
%%~name: Main
|
||||||
%%~path: 6c6afb1247750/2426c6f0ca922
|
%%~path: 6c6afb1247750/2426c6f0ca922
|
||||||
%%~kind: PLOT/NOTE
|
%%~kind: PLOT/NOTE
|
||||||
|
%%~hash: db3897d166e246acdb5e25e9bd98c5a40a699ed0
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
# Main Plot
|
# Main Plot
|
||||||
|
|
||||||
@tag: Main
|
@tag: Main
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Chapter Two
|
%%~name: Chapter Two
|
||||||
%%~path: 6bd935d2490cd/441420a886d82
|
%%~path: 6bd935d2490cd/441420a886d82
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: fd6d46708faa1333f8f7ba0442fc5b35bf1e3f85
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
## Chapter Two
|
## Chapter Two
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Scene Five
|
%%~name: Scene Five
|
||||||
%%~path: 6bd935d2490cd/47666c91c7ccf
|
%%~path: 6bd935d2490cd/47666c91c7ccf
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: d210c26966da6f9edea8726567abf4860b7bb9b7
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
### Scene Five
|
### Scene Five
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Mr. Nobody
|
%%~name: Mr. Nobody
|
||||||
%%~path: 67a8707f2f249/4c4f28287af27
|
%%~path: 67a8707f2f249/4c4f28287af27
|
||||||
%%~kind: CHARACTER/NOTE
|
%%~kind: CHARACTER/NOTE
|
||||||
|
%%~hash: 07a86c2001669de7d7a7545286c22b963e3ccf63
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
# Nobody Owens
|
# Nobody Owens
|
||||||
|
|
||||||
@tag: Bod
|
@tag: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Lorem Ipsum
|
%%~name: Lorem Ipsum
|
||||||
%%~path: b3643d0f92e32/7a992350f3eb6
|
%%~path: b3643d0f92e32/7a992350f3eb6
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 8efda028000b70be0d7dbe9647b6026082ef05c9
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
#! Lorem Ipsum
|
#! Lorem Ipsum
|
||||||
|
|
||||||
>> **By lipsum.com** <<
|
>> **By lipsum.com** <<
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Interlude
|
%%~name: Interlude
|
||||||
%%~path: b3643d0f92e32/846352075de7d
|
%%~path: b3643d0f92e32/846352075de7d
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: ac0e16c65142b9f1e0fa281bdc9b954e44026740
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
##! Why do we use it?
|
##! Why do we use it?
|
||||||
|
|
||||||
% Exctracted from the lipsum.com website.
|
% Exctracted from the lipsum.com website.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Scene One
|
%%~name: Scene One
|
||||||
%%~path: 45e6b01ca35c1/88243afbe5ed8
|
%%~path: 45e6b01ca35c1/88243afbe5ed8
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: a09245a7a772bbe02850b5db109977e336cd9cc1
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
### Scene One
|
### Scene One
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Prologue
|
%%~name: Prologue
|
||||||
%%~path: b3643d0f92e32/88d59a277361b
|
%%~path: b3643d0f92e32/88d59a277361b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 19a2aa95b07ce10eaea753c46569929439648c63
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
##! Prologue
|
##! Prologue
|
||||||
|
|
||||||
% Synopsis:Explanation from the lipsum.com website.
|
% Synopsis:Explanation from the lipsum.com website.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Front Matter
|
%%~name: Front Matter
|
||||||
%%~path: b3643d0f92e32/8c58a65414c23
|
%%~path: b3643d0f92e32/8c58a65414c23
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 5c3961cb7616ef2b378010f38a89ed268ef15d92
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
[NEW PAGE]
|
[NEW PAGE]
|
||||||
|
|
||||||
% Exctracted from the lipsum.com website.
|
% Exctracted from the lipsum.com website.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Act One
|
%%~name: Act One
|
||||||
%%~path: b3643d0f92e32/db7e733775d4d
|
%%~path: b3643d0f92e32/db7e733775d4d
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: d93cd4c96d49e4afd93c82cca29d413a35012108
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
# Act One
|
# Act One
|
||||||
|
|
||||||
>> “Fusce maximus felis libero” <<
|
>> “Fusce maximus felis libero” <<
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Scene Three
|
%%~name: Scene Three
|
||||||
%%~path: 6bd935d2490cd/eb103bc70c90c
|
%%~path: 6bd935d2490cd/eb103bc70c90c
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: c4eda49e4fe81dc450d547eee0bdabe77fdaaa98
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
### Scene Three
|
### Scene Three
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Scene Four
|
%%~name: Scene Four
|
||||||
%%~path: 6bd935d2490cd/f8c0562e50f1b
|
%%~path: 6bd935d2490cd/f8c0562e50f1b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 9461a279b9fb6ef005ee4d432fcda77ff5bfbd42
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
### Scene Four
|
### Scene Four
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Scene Two
|
%%~name: Scene Two
|
||||||
%%~path: 45e6b01ca35c1/f96ec11c6a3da
|
%%~path: 45e6b01ca35c1/f96ec11c6a3da
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: ebe3fbaa16d9d81bc1a139822e3bf39bb357866d
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
### Scene Two
|
### Scene Two
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Chapter One
|
%%~name: Chapter One
|
||||||
%%~path: 45e6b01ca35c1/fb609cd8319dc
|
%%~path: 45e6b01ca35c1/fb609cd8319dc
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 5dabeaa7a58238a6ad99ce73176b1bdfad1a71b7
|
||||||
|
%%~date: Unknown/Unknown
|
||||||
## Chapter One
|
## Chapter One
|
||||||
|
|
||||||
@pov: Bod
|
@pov: Bod
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:18:14">
|
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-08-25 18:03:37">
|
||||||
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="42" autoCount="24" editTime="1913">
|
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="43" autoCount="24" editTime="1935">
|
||||||
<name>Lorem Ipsum</name>
|
<name>Lorem Ipsum</name>
|
||||||
<title>Lorem Ipsum</title>
|
<title>Lorem Ipsum</title>
|
||||||
<author>lipsum.com</author>
|
<author>lipsum.com</author>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: Chapter 1
|
%%~name: Chapter 1
|
||||||
%%~path: 0000000000008/0000000000010
|
%%~path: 0000000000008/0000000000010
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 25af8567c8a3b3b49a27dcda2999ed1f8811955a
|
||||||
|
%%~date: 2023-08-25 18:32:30/2023-08-25 18:32:30
|
||||||
## Chapter 1
|
## Chapter 1
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: All of Chapter 1
|
%%~name: All of Chapter 1
|
||||||
%%~path: 0000000000008/0000000000014
|
%%~path: 0000000000008/0000000000014
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: 9e632e7e29860572c685da8501b92bf99825ac3b
|
||||||
|
%%~date: 2023-08-25 18:29:39/2023-08-25 18:29:39
|
||||||
% Merge Novel Chapter: Chapter 1 [New]
|
% Merge Novel Chapter: Chapter 1 [New]
|
||||||
|
|
||||||
## Chapter 1
|
## Chapter 1
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: New Scene
|
%%~name: New Scene
|
||||||
%%~path: 000000000000d/000000000000f
|
%%~path: 000000000000d/000000000000f
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
|
%%~hash: fd5dc2f0c9767cb124b1bf2300d7a33b7780045e
|
||||||
|
%%~date: 2023-08-25 18:08:01/2023-08-25 18:08:04
|
||||||
# Novel
|
# Novel
|
||||||
|
|
||||||
## Chapter
|
## Chapter
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: New Note
|
%%~name: New Note
|
||||||
%%~path: 000000000000a/0000000000010
|
%%~path: 000000000000a/0000000000010
|
||||||
%%~kind: CHARACTER/NOTE
|
%%~kind: CHARACTER/NOTE
|
||||||
|
%%~hash: 9fae6dfdd3d1c0822d3a3cf90c0142e65ad8e557
|
||||||
|
%%~date: 2023-08-25 18:14:24/2023-08-25 18:14:24
|
||||||
# Jane Doe
|
# Jane Doe
|
||||||
|
|
||||||
@tag: Jane
|
@tag: Jane
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: New Note
|
%%~name: New Note
|
||||||
%%~path: 0000000000009/0000000000011
|
%%~path: 0000000000009/0000000000011
|
||||||
%%~kind: PLOT/NOTE
|
%%~kind: PLOT/NOTE
|
||||||
|
%%~hash: 8ff26f8a18ad6390c2ce725c441ab0c792a125cf
|
||||||
|
%%~date: 2023-08-25 18:15:35/2023-08-25 18:15:35
|
||||||
# Main Plot
|
# Main Plot
|
||||||
|
|
||||||
@tag: MainPlot
|
@tag: MainPlot
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
%%~name: New Note
|
%%~name: New Note
|
||||||
%%~path: 000000000000b/0000000000012
|
%%~path: 000000000000b/0000000000012
|
||||||
%%~kind: WORLD/NOTE
|
%%~kind: WORLD/NOTE
|
||||||
|
%%~hash: 3f5c3c6c3ba1c27c30b8ac9e59c222fb9a1bd775
|
||||||
|
%%~date: 2023-08-25 18:17:45/2023-08-25 18:17:45
|
||||||
# Main Location
|
# Main Location
|
||||||
|
|
||||||
@tag: Home
|
@tag: Home
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
@@ -35,8 +34,8 @@ from novelwriter.common import (
|
|||||||
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
||||||
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
|
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
|
||||||
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
||||||
numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified,
|
numberToRoman, NWConfigParser, readTextFile, simplified, transferCase,
|
||||||
transferCase, xmlIndent, yesNo
|
xmlIndent, yesNo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -622,45 +621,6 @@ def testBaseCommon_makeFileNameSafe():
|
|||||||
# END Test testBaseCommon_makeFileNameSafe
|
# END Test testBaseCommon_makeFileNameSafe
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
|
||||||
def testBaseCommon_sha256sum(monkeypatch, fncPath, ipsumText):
|
|
||||||
"""Test the sha256sum function."""
|
|
||||||
longText = 50*(" ".join(ipsumText) + " ")
|
|
||||||
shortText = "This is a short file"
|
|
||||||
noneText = ""
|
|
||||||
|
|
||||||
assert len(longText) == 175650
|
|
||||||
|
|
||||||
longFile = fncPath / "long_file.txt"
|
|
||||||
shortFile = fncPath / "short_file.txt"
|
|
||||||
noneFile = fncPath / "none_file.txt"
|
|
||||||
|
|
||||||
writeFile(longFile, longText)
|
|
||||||
writeFile(shortFile, shortText)
|
|
||||||
writeFile(noneFile, noneText)
|
|
||||||
|
|
||||||
# Taken with sha256sum command on command line
|
|
||||||
longHash = "9b22aee35660da4fae204acbe96aec7f563022746ca2b7a3831f5e44544765eb"
|
|
||||||
shortHash = "6d7c9b2722364c471b8a8666bcb35d18500272d05b23b3427288e2e34c6618f0"
|
|
||||||
noneHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
||||||
|
|
||||||
assert sha256sum(longFile) == longHash
|
|
||||||
assert sha256sum(shortFile) == shortHash
|
|
||||||
assert sha256sum(noneFile) == noneHash
|
|
||||||
|
|
||||||
assert hashlib.sha256(longText.encode("utf-8")).hexdigest() == longHash
|
|
||||||
assert hashlib.sha256(shortText.encode("utf-8")).hexdigest() == shortHash
|
|
||||||
assert hashlib.sha256(noneText.encode("utf-8")).hexdigest() == noneHash
|
|
||||||
|
|
||||||
with monkeypatch.context() as mp:
|
|
||||||
mp.setattr("builtins.open", causeOSError)
|
|
||||||
assert sha256sum(longFile) is None
|
|
||||||
assert sha256sum(shortFile) is None
|
|
||||||
assert sha256sum(noneFile) is None
|
|
||||||
|
|
||||||
# END Test testBaseCommon_sha256sum
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_getGuiItem(nwGUI):
|
def testBaseCommon_getGuiItem(nwGUI):
|
||||||
"""Check the GUI item function."""
|
"""Check the GUI item function."""
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ from shutil import copyfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE
|
||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
from tools import C, buildTestProject, cmpFiles, XML_IGNORE
|
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.constants import nwItemClass
|
from novelwriter.constants import nwItemClass
|
||||||
@@ -46,19 +46,19 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
|||||||
# =====================
|
# =====================
|
||||||
|
|
||||||
hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
|
hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
|
||||||
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
|
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1) # type: ignore
|
||||||
hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1)
|
hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1) # type: ignore
|
||||||
hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1)
|
hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1) # type: ignore
|
||||||
|
|
||||||
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
|
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
|
||||||
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
|
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
|
||||||
docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
|
docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
|
||||||
docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
|
docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
|
||||||
|
|
||||||
theProject.writeNewFile(hChapter1, 2, True, docText1)
|
theProject.writeNewFile(hChapter1, 2, True, docText1) # type: ignore
|
||||||
theProject.writeNewFile(hSceneOne11, 3, True, docText2)
|
theProject.writeNewFile(hSceneOne11, 3, True, docText2) # type: ignore
|
||||||
theProject.writeNewFile(hSceneOne12, 3, True, docText3)
|
theProject.writeNewFile(hSceneOne12, 3, True, docText3) # type: ignore
|
||||||
theProject.writeNewFile(hSceneOne13, 3, True, docText4)
|
theProject.writeNewFile(hSceneOne13, 3, True, docText4) # type: ignore
|
||||||
|
|
||||||
# Basic Checks
|
# Basic Checks
|
||||||
# ============
|
# ============
|
||||||
@@ -81,12 +81,12 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
|||||||
testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000014.nwd"
|
testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000014.nwd"
|
||||||
compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000014.nwd"
|
compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000014.nwd"
|
||||||
|
|
||||||
assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014"
|
assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014" # type: ignore
|
||||||
|
|
||||||
assert docMerger.appendText(hChapter1, True, "Merge") is True
|
assert docMerger.appendText(hChapter1, True, "Merge") is True # type: ignore
|
||||||
assert docMerger.appendText(hSceneOne11, True, "Merge") is True
|
assert docMerger.appendText(hSceneOne11, True, "Merge") is True # type: ignore
|
||||||
assert docMerger.appendText(hSceneOne12, True, "Merge") is True
|
assert docMerger.appendText(hSceneOne12, True, "Merge") is True # type: ignore
|
||||||
assert docMerger.appendText(hSceneOne13, True, "Merge") is True
|
assert docMerger.appendText(hSceneOne13, True, "Merge") is True # type: ignore
|
||||||
|
|
||||||
# Block writing and check error handling
|
# Block writing and check error handling
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
@@ -98,7 +98,7 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
|||||||
# Write properly, and compare
|
# Write properly, and compare
|
||||||
assert docMerger.writeTargetDoc() is True
|
assert docMerger.writeTargetDoc() is True
|
||||||
copyfile(saveFile, testFile)
|
copyfile(saveFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile)
|
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||||
|
|
||||||
# Merge into Existing
|
# Merge into Existing
|
||||||
# ===================
|
# ===================
|
||||||
@@ -107,15 +107,15 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
|||||||
testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000010.nwd"
|
testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000010.nwd"
|
||||||
compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000010.nwd"
|
compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000010.nwd"
|
||||||
|
|
||||||
docMerger.setTargetDoc(hChapter1)
|
docMerger.setTargetDoc(hChapter1) # type: ignore
|
||||||
|
|
||||||
assert docMerger.appendText(hSceneOne11, True, "Merge") is True
|
assert docMerger.appendText(hSceneOne11, True, "Merge") is True # type: ignore
|
||||||
assert docMerger.appendText(hSceneOne12, True, "Merge") is True
|
assert docMerger.appendText(hSceneOne12, True, "Merge") is True # type: ignore
|
||||||
assert docMerger.appendText(hSceneOne13, True, "Merge") is True
|
assert docMerger.appendText(hSceneOne13, True, "Merge") is True # type: ignore
|
||||||
|
|
||||||
assert docMerger.writeTargetDoc() is True
|
assert docMerger.writeTargetDoc() is True
|
||||||
copyfile(saveFile, testFile)
|
copyfile(saveFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile)
|
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||||
|
|
||||||
# Just for debugging
|
# Just for debugging
|
||||||
docMerger.writeTargetDoc()
|
docMerger.writeTargetDoc()
|
||||||
@@ -163,11 +163,11 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText)
|
|||||||
docText = "\n\n".join(docData)
|
docText = "\n\n".join(docData)
|
||||||
docRaw = docText.splitlines()
|
docRaw = docText.splitlines()
|
||||||
assert theProject.storage.getDocument(hSplitDoc).writeDocument(docText) is True
|
assert theProject.storage.getDocument(hSplitDoc).writeDocument(docText) is True
|
||||||
theProject.tree[hSplitDoc].setStatus(C.sFinished)
|
theProject.tree[hSplitDoc].setStatus(C.sFinished) # type: ignore
|
||||||
theProject.tree[hSplitDoc].setImport(C.iMain)
|
theProject.tree[hSplitDoc].setImport(C.iMain) # type: ignore
|
||||||
|
|
||||||
docSplitter = DocSplitter(theProject, hSplitDoc)
|
docSplitter = DocSplitter(theProject, hSplitDoc) # type: ignore
|
||||||
assert docSplitter._srcItem.isFileType()
|
assert docSplitter._srcItem.isFileType() # type: ignore
|
||||||
assert docSplitter.getError() == ""
|
assert docSplitter.getError() == ""
|
||||||
|
|
||||||
# Run the split algorithm
|
# Run the split algorithm
|
||||||
@@ -247,8 +247,8 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText)
|
|||||||
|
|
||||||
# Check that status and importance has been preserved
|
# Check that status and importance has been preserved
|
||||||
for rHandle in resDocHandle:
|
for rHandle in resDocHandle:
|
||||||
assert theProject.tree[rHandle].itemStatus == C.sFinished
|
assert theProject.tree[rHandle].itemStatus == C.sFinished # type: ignore
|
||||||
assert theProject.tree[rHandle].itemImport == C.iMain
|
assert theProject.tree[rHandle].itemImport == C.iMain # type: ignore
|
||||||
|
|
||||||
# Check handling of improper initialisation
|
# Check handling of improper initialisation
|
||||||
docSplitter = DocSplitter(theProject, C.hInvalid)
|
docSplitter = DocSplitter(theProject, C.hInvalid)
|
||||||
@@ -416,7 +416,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
|
|||||||
assert projBuild.buildProject({}) is False
|
assert projBuild.buildProject({}) is False
|
||||||
|
|
||||||
# Wrong type should also fail
|
# Wrong type should also fail
|
||||||
assert projBuild.buildProject("stuff") is False
|
assert projBuild.buildProject("stuff") is False # type: ignore
|
||||||
|
|
||||||
# Try again with a proper path
|
# Try again with a proper path
|
||||||
assert projBuild.buildProject({"projPath": fncPath}) is True
|
assert projBuild.buildProject({"projPath": fncPath}) is True
|
||||||
@@ -508,7 +508,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockRnd):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths):
|
def testCoreTools_NewSample(monkeypatch, mockGUI, fncPath, tstPaths):
|
||||||
"""Check that we can create a new project can be created from the
|
"""Check that we can create a new project can be created from the
|
||||||
provided sample project via a zip file.
|
provided sample project via a zip file.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tools import C, MOCK_TIME, buildTestProject, readFile, writeFile
|
||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
from tools import C, buildTestProject, readFile, writeFile
|
|
||||||
|
|
||||||
from novelwriter.enum import nwItemClass, nwItemLayout
|
from novelwriter.enum import nwItemClass, nwItemLayout
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
@@ -32,6 +32,8 @@ from novelwriter.core.document import NWDocument
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||||
"""Test loading and saving a document with the NWDocument class."""
|
"""Test loading and saving a document with the NWDocument class."""
|
||||||
|
monkeypatch.setattr("novelwriter.core.document.time", lambda: MOCK_TIME)
|
||||||
|
|
||||||
theProject = NWProject()
|
theProject = NWProject()
|
||||||
mockRnd.reset()
|
mockRnd.reset()
|
||||||
buildTestProject(theProject, fncPath)
|
buildTestProject(theProject, fncPath)
|
||||||
@@ -48,7 +50,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
# Non-existent handle
|
# Non-existent handle
|
||||||
theDoc = NWDocument(theProject, C.hInvalid)
|
theDoc = NWDocument(theProject, C.hInvalid)
|
||||||
assert theDoc.readDocument() is None
|
assert theDoc.readDocument() is None
|
||||||
assert theDoc._currHash is None
|
assert theDoc._lastHash == ""
|
||||||
assert theDoc.fileExists() is False
|
assert theDoc.fileExists() is False
|
||||||
|
|
||||||
# No content path
|
# No content path
|
||||||
@@ -90,7 +92,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
# Set handle and save
|
# Set handle and save
|
||||||
theText = "### Test File\n\nText ...\n\n"
|
theText = "### Test File\n\nText ...\n\n"
|
||||||
theDoc = NWDocument(theProject, xHandle)
|
theDoc = NWDocument(theProject, xHandle)
|
||||||
assert theDoc.readDocument(xHandle) == ""
|
assert theDoc.readDocument(xHandle) == "" # type: ignore
|
||||||
assert theDoc.writeDocument(theText) is True
|
assert theDoc.writeDocument(theText) is True
|
||||||
|
|
||||||
# Save again to ensure temp file and previous file is handled
|
# Save again to ensure temp file and previous file is handled
|
||||||
@@ -102,6 +104,8 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
"%%~name: New File\n"
|
"%%~name: New File\n"
|
||||||
f"%%~path: {C.hNovelRoot}/{xHandle}\n"
|
f"%%~path: {C.hNovelRoot}/{xHandle}\n"
|
||||||
"%%~kind: NOVEL/DOCUMENT\n"
|
"%%~kind: NOVEL/DOCUMENT\n"
|
||||||
|
"%%~hash: b288c3ab03181027d9a16d7fd2291262f5de9ac8\n"
|
||||||
|
"%%~date: 2019-05-10 18:52:00/2019-05-10 18:52:00\n"
|
||||||
"### Test File\n\n"
|
"### Test File\n\n"
|
||||||
"Text ...\n\n"
|
"Text ...\n\n"
|
||||||
)
|
)
|
||||||
@@ -114,7 +118,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
assert theDoc.writeDocument(theText, forceWrite=True) is True
|
assert theDoc.writeDocument(theText, forceWrite=True) is True
|
||||||
|
|
||||||
# Force no meta data
|
# Force no meta data
|
||||||
theDoc._theItem = None
|
theDoc._item = None
|
||||||
assert theDoc.writeDocument(theText) is True
|
assert theDoc.writeDocument(theText) is True
|
||||||
assert readFile(docPath) == theText
|
assert readFile(docPath) == theText
|
||||||
|
|
||||||
@@ -137,7 +141,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
assert theDoc.getError() == ""
|
assert theDoc.getError() == ""
|
||||||
|
|
||||||
# Saving with no handle
|
# Saving with no handle
|
||||||
theDoc._docHandle = None
|
theDoc._handle = None
|
||||||
assert theDoc.writeDocument(theText) is False
|
assert theDoc.writeDocument(theText) is False
|
||||||
|
|
||||||
# Delete Document
|
# Delete Document
|
||||||
@@ -170,9 +174,10 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
|
def testCoreDocument_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||||
"""Test other methods of the NWDocument class.
|
"""Test other methods of the NWDocument class."""
|
||||||
"""
|
monkeypatch.setattr("novelwriter.core.document.time", lambda: MOCK_TIME)
|
||||||
|
|
||||||
theProject = NWProject()
|
theProject = NWProject()
|
||||||
mockRnd.reset()
|
mockRnd.reset()
|
||||||
buildTestProject(theProject, fncPath)
|
buildTestProject(theProject, fncPath)
|
||||||
@@ -187,7 +192,7 @@ def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
|
|||||||
|
|
||||||
# Check the item
|
# Check the item
|
||||||
assert theDoc.getCurrentItem() is not None
|
assert theDoc.getCurrentItem() is not None
|
||||||
assert theDoc.getCurrentItem().itemHandle == C.hSceneDoc
|
assert theDoc.getCurrentItem().itemHandle == C.hSceneDoc # type: ignore
|
||||||
|
|
||||||
# Check the meta
|
# Check the meta
|
||||||
theName, theParent, theClass, theLayout = theDoc.getMeta()
|
theName, theParent, theClass, theLayout = theDoc.getMeta()
|
||||||
@@ -202,6 +207,8 @@ def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
|
|||||||
"%%~name: New Scene\n"
|
"%%~name: New Scene\n"
|
||||||
f"%%~path: {C.hChapterDir}/{C.hSceneDoc}\n"
|
f"%%~path: {C.hChapterDir}/{C.hSceneDoc}\n"
|
||||||
"%%~kind: NOVEL/DOCUMENT\n"
|
"%%~kind: NOVEL/DOCUMENT\n"
|
||||||
|
"%%~hash: dd350c602de803554b2a7c17f191ae25dea1df63\n"
|
||||||
|
"%%~date: 2019-05-10 18:52:00/2019-05-10 18:52:00\n"
|
||||||
"%%~ stuff\n"
|
"%%~ stuff\n"
|
||||||
"### Test File\n\n"
|
"### Test File\n\n"
|
||||||
"Text ...\n\n"
|
"Text ...\n\n"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import pytest
|
|||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
from tools import (
|
from tools import (
|
||||||
C, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem, writeFile
|
C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem, writeFile
|
||||||
)
|
)
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@@ -529,25 +529,25 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_000000000000f.nwd"
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_000000000000f.nwd"
|
||||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_000000000000f.nwd"
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_000000000000f.nwd"
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile)
|
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||||
|
|
||||||
projFile = projPath / "content" / "0000000000010.nwd"
|
projFile = projPath / "content" / "0000000000010.nwd"
|
||||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000010.nwd"
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000010.nwd"
|
||||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000010.nwd"
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000010.nwd"
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile)
|
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||||
|
|
||||||
projFile = projPath / "content" / "0000000000011.nwd"
|
projFile = projPath / "content" / "0000000000011.nwd"
|
||||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000011.nwd"
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000011.nwd"
|
||||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000011.nwd"
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000011.nwd"
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile)
|
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||||
|
|
||||||
projFile = projPath / "content" / "0000000000012.nwd"
|
projFile = projPath / "content" / "0000000000012.nwd"
|
||||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000012.nwd"
|
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000012.nwd"
|
||||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000012.nwd"
|
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000012.nwd"
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, compFile)
|
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -23,11 +23,14 @@ from __future__ import annotations
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtWidgets import qApp
|
from PyQt5.QtWidgets import qApp
|
||||||
|
|
||||||
XML_IGNORE = ("<novelWriterXML", "<project")
|
XML_IGNORE = ("<novelWriterXML", "<project")
|
||||||
ODT_IGNORE = ("<meta:generator", "<meta:creation-date", "<dc:date", "<meta:editing")
|
ODT_IGNORE = ("<meta:generator", "<meta:creation-date", "<dc:date", "<meta:editing")
|
||||||
|
NWD_IGNORE = ("%%~date:",)
|
||||||
|
MOCK_TIME = datetime(2019, 5, 10, 18, 52, 0).timestamp()
|
||||||
|
|
||||||
|
|
||||||
class C:
|
class C:
|
||||||
@@ -62,8 +65,7 @@ def cmpFiles(
|
|||||||
ignoreLines: list | None = None,
|
ignoreLines: list | None = None,
|
||||||
ignoreStart: tuple | None = None
|
ignoreStart: tuple | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Compare two files, but optionally ignore lines given by a list.
|
"""Compare two files, with optional line ignore."""
|
||||||
"""
|
|
||||||
if ignoreLines is None:
|
if ignoreLines is None:
|
||||||
ignoreLines = []
|
ignoreLines = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user