Merge pull request #620 from vkbo/item_layouts

Automatic Item Layouts
This commit is contained in:
Veronica K. Berglyd Olsen
2021-01-30 14:16:14 +00:00
committed by GitHub
8 changed files with 364 additions and 41 deletions
+36 -3
View File
@@ -59,6 +59,7 @@ class NWIndex():
self._novelIndex = {}
self._noteIndex = {}
self._textCounts = {}
self._firstTitle = {}
# TimeStamps
self._timeNovel = 0
@@ -79,6 +80,7 @@ class NWIndex():
self._novelIndex = {}
self._noteIndex = {}
self._textCounts = {}
self._firstTitle = {}
self._timeNovel = 0
self._timeNotes = 0
self._timeIndex = 0
@@ -101,6 +103,7 @@ class NWIndex():
self._novelIndex.pop(tHandle, None)
self._noteIndex.pop(tHandle, None)
self._textCounts.pop(tHandle, None)
self._firstTitle.pop(tHandle, None)
return
@@ -169,6 +172,7 @@ class NWIndex():
self._novelIndex = theData.get("novelIndex", {})
self._noteIndex = theData.get("noteIndex", {})
self._textCounts = theData.get("textCounts", {})
self._firstTitle = theData.get("firstTitle", {})
nowTime = round(time())
self._timeNovel = nowTime
@@ -194,6 +198,7 @@ class NWIndex():
"novelIndex" : self._novelIndex,
"noteIndex" : self._noteIndex,
"textCounts" : self._textCounts,
"firstTitle" : self._firstTitle,
}, outFile, indent=2)
except Exception:
logger.error("Failed to save index file")
@@ -215,6 +220,7 @@ class NWIndex():
self._checkNovelNoteIndex("novelIndex")
self._checkNovelNoteIndex("noteIndex")
self._checkTextCounts()
self._checkFirstTitles()
self.indexBroken = False
except Exception:
@@ -285,6 +291,7 @@ class NWIndex():
"tags" : [],
"updated" : round(time()),
}
self._firstTitle[tHandle] = ["H0", "T000000"]
if itemLayout == nwItemLayout.NOTE:
self._novelIndex.pop(tHandle, None)
self._noteIndex[tHandle] = {}
@@ -312,7 +319,7 @@ class NWIndex():
if nChar == 0:
continue
if aLine.startswith(r"#"):
if aLine.startswith("#"):
isTitle = self._indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
if isTitle and nLine > 0:
if nTitle > 0:
@@ -320,11 +327,11 @@ class NWIndex():
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
nTitle = nLine
elif aLine.startswith(r"@"):
elif aLine.startswith("@"):
self._indexNoteRef(tHandle, aLine, nLine, nTitle)
self._indexTag(tHandle, aLine, nLine, nTitle, itemClass)
elif aLine.startswith(r"%"):
elif aLine.startswith("%"):
if nTitle > 0:
toCheck = aLine[1:].lstrip()
synTag = toCheck[:9].lower()
@@ -393,6 +400,9 @@ class NWIndex():
"updated" : round(time()),
}
if self._firstTitle[tHandle][0] == "H0":
self._firstTitle[tHandle] = [hDepth, sTitle]
if hText != "":
if isNovel:
if tHandle in self._novelIndex:
@@ -638,6 +648,11 @@ class NWIndex():
return theToC
def getFirstTitle(self, tHandle):
"""Return the level and location of the first title of a handle.
"""
return self._firstTitle.get(tHandle, ["H0", "T000000"])
def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file
starting at title sTitle if it is provided.
@@ -879,4 +894,22 @@ class NWIndex():
return
def _checkFirstTitles(self):
"""Scan the first titles index for errors.
Waring: This function raises exceptions.
"""
for tHandle in self._firstTitle:
if not isHandle(tHandle):
raise KeyError("firstTitle key is not a handle")
tEntry = self._firstTitle[tHandle]
if len(tEntry) != 2:
raise IndexError("firstTitle[a] expected 2 values")
if not tEntry[0] in self.H_VALID:
raise ValueError("firstTitle[a][0] is not a header level")
if not isTitleTag(tEntry[1]):
raise ValueError("firstTitle[a][1] is not a title tag")
return
# END Class NWIndex
+49 -1
View File
@@ -34,10 +34,35 @@ from time import time
from nw.core.item import NWItem
from nw.common import checkHandle
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst, nwLists
)
logger = logging.getLogger(__name__)
# Layout Translation Map
LAYOUT_MAP = {
nwItemLayout.SCENE: {
"H1": nwItemLayout.BOOK,
"H2": nwItemLayout.CHAPTER,
},
nwItemLayout.CHAPTER: {
"H1": nwItemLayout.BOOK,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
nwItemLayout.UNNUMBERED: {
"H1": nwItemLayout.BOOK,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
nwItemLayout.PARTITION: {
"H2": nwItemLayout.CHAPTER,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
}
class NWTree():
def __init__(self, theProject):
@@ -204,6 +229,29 @@ class NWTree():
novelWords += tItem.wordCount
return novelWords, noteWords
def updateItemLayout(self, tHandle, hLevel):
"""Check if the item layout needs updating based on the header
given level.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
if tItem.itemClass not in nwLists.CLS_NOVEL:
return False
if hLevel not in ("H1", "H2", "H3", "H4"):
return False
iLayout = tItem.itemLayout
if iLayout in LAYOUT_MAP:
if hLevel in LAYOUT_MAP[iLayout]:
tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel]
logger.debug("Changed layout for %s from %s to %s" % (
tHandle, iLayout.name, tItem.itemLayout.name
))
return True
return False
##
# Tree Structure Methods
##
+7 -1
View File
@@ -398,6 +398,7 @@ class GuiDocEditor(QTextEdit):
return False
docText = self.getText()
tHandle = theItem.itemHandle
cC, wC, pC = countWords(docText)
self._updateCounts(cC, wC, pC)
@@ -410,7 +411,12 @@ class GuiDocEditor(QTextEdit):
self.nwDocument.saveDocument(docText)
self.setDocumentChanged(False)
self.theParent.theIndex.scanText(theItem.itemHandle, docText)
self.theParent.theIndex.scanText(tHandle, docText)
hLevel, _ = self.theParent.theIndex.getFirstTitle(tHandle)
if self.theProject.projTree.updateItemLayout(tHandle, hLevel):
self.theParent.treeView.setTreeItemValues(tHandle)
self.nwDocument.saveDocument(docText)
return True
@@ -567,5 +567,67 @@
259,
3
]
},
"firstTitle": {
"7a992350f3eb6": [
"H1",
"T000001"
],
"8c58a65414c23": [
"H0",
"T000000"
],
"88d59a277361b": [
"H2",
"T000001"
],
"db7e733775d4d": [
"H1",
"T000001"
],
"fb609cd8319dc": [
"H2",
"T000001"
],
"88243afbe5ed8": [
"H3",
"T000001"
],
"f96ec11c6a3da": [
"H3",
"T000001"
],
"846352075de7d": [
"H2",
"T000001"
],
"441420a886d82": [
"H2",
"T000001"
],
"eb103bc70c90c": [
"H3",
"T000001"
],
"f8c0562e50f1b": [
"H3",
"T000001"
],
"47666c91c7ccf": [
"H3",
"T000001"
],
"4c4f28287af27": [
"H1",
"T000001"
],
"2426c6f0ca922": [
"H1",
"T000001"
],
"04468803b92e1": [
"H1",
"T000001"
]
}
}
@@ -1,6 +1,6 @@
%%~name: New Scene
%%~path: 31489056e0916/0e17daca5f3e1
%%~kind: NOVEL/SCENE
%%~kind: NOVEL/BOOK
# Novel
## Chapter
@@ -1,11 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.0b3" hexVersion="0x010000b3" fileVersion="1.2" timeStamp="2020-09-21 19:58:53">
<novelWriterXML appVersion="1.2a0" hexVersion="0x010200a0" fileVersion="1.2" timeStamp="2021-01-29 01:10:23">
<project>
<name>New Project</name>
<title></title>
<saveCount>4</saveCount>
<autoCount>1</autoCount>
<editTime>11</editTime>
<autoCount>2</autoCount>
<editTime>8</editTime>
</project>
<settings>
<doBackup>True</doBackup>
@@ -83,7 +83,7 @@
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<layout>BOOK</layout>
<charCount>466</charCount>
<wordCount>83</wordCount>
<paraCount>4</paraCount>
+66 -17
View File
@@ -492,9 +492,8 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert wC == 12 # Words in text and title only
assert pC == 2 # Paragraphs in text only
##
# getReferences
##
# getReferences
# =============
# Look up an ivalid handle
theRefs = theIndex.getReferences("Not a handle")
@@ -506,9 +505,8 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert theRefs["@pov"] == ["Jane"]
assert theRefs["@char"] == ["Jane"]
##
# getBackReferenceList
##
# getBackReferenceList
# ====================
# None handle should return an empty dict
assert theIndex.getBackReferenceList(None) == {}
@@ -517,16 +515,15 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
theRefs = theIndex.getBackReferenceList(cHandle)
assert theRefs == {nHandle: "T000001"}
##
# getTagSource
##
# getTagSource
# ============
assert theIndex.getTagSource("Jane") == (cHandle, 2, "T000001")
assert theIndex.getTagSource("John") == (None, 0, "T000000")
##
# getCounts for whole text and sections
##
# getCounts
# =========
# For whole text and sections
# Get section counts for a novel file
assert theIndex.scanText(nHandle, (
@@ -555,7 +552,7 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert wC == 12
assert pC == 2
# First part
# Second part
cC, wC, pC = theIndex.getCounts(nHandle, "T000011")
assert cC == 62
assert wC == 12
@@ -588,15 +585,19 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert wC == 12
assert pC == 2
# First part
# Second part
cC, wC, pC = theIndex.getCounts(cHandle, "T000011")
assert cC == 62
assert wC == 12
assert pC == 2
##
# Novel Stats
##
# getFirstTitle
# =============
assert theIndex.getFirstTitle(cHandle) == ["H1", "T000001"]
# Novel Stats
# ===========
hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c")
sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c")
@@ -1145,3 +1146,51 @@ def testCoreIndex_CheckTextCounts(dummyGUI):
theIndex._checkTextCounts()
# END Test testCoreIndex_CheckTextCounts
@pytest.mark.core
def testCoreIndex_CheckFirstTitle(dummyGUI):
"""Test the first title checker.
"""
theProject = NWProject(dummyGUI)
theIndex = NWIndex(theProject, dummyGUI)
# Valid Index
theIndex._firstTitle = {
"53b69b83cdafc": ["H1", "T000001"],
"974e400180a99": ["H0", "T000000"],
}
assert theIndex._checkFirstTitles() is None
# Invalid Handle
theIndex._firstTitle = {
"53b69b83cdafc": ["H1", "T000001"],
"h74e400180a99": ["H0", "T000000"],
}
with pytest.raises(KeyError):
theIndex._checkFirstTitles()
# Wrong Length
theIndex._firstTitle = {
"53b69b83cdafc": ["H1", "T000001"],
"974e400180a99": ["H0", "T000000", "stuff"],
}
with pytest.raises(IndexError):
theIndex._checkFirstTitles()
# Wrong Header
theIndex._firstTitle = {
"53b69b83cdafc": ["H1", "T000001"],
"974e400180a99": ["XX", "T000000"],
}
with pytest.raises(ValueError):
theIndex._checkFirstTitles()
# Wrong Title
theIndex._firstTitle = {
"53b69b83cdafc": ["H1", "T000001"],
"974e400180a99": ["H0", "INVALID"],
}
with pytest.raises(ValueError):
theIndex._checkFirstTitles()
# END Test testCoreIndex_CheckFirstTitle
+139 -14
View File
@@ -123,9 +123,9 @@ def testCoreTree_BuildTree(dummyGUI, dummyItems):
assert not theTree.isTrashRoot("a000000000003")
aHandles = []
for tHandle, pHande, nwItem in dummyItems:
for tHandle, pHandle, nwItem in dummyItems:
aHandles.append(tHandle)
assert theTree.append(tHandle, pHande, nwItem)
assert theTree.append(tHandle, pHandle, nwItem)
assert theTree._treeChanged
@@ -202,13 +202,13 @@ def testCoreTree_BuildTree(dummyGUI, dummyItems):
@pytest.mark.core
def testCoreTree_Methods(dummyGUI, dummyItems):
"""Test building a project tree from a list of items.
"""Test bvarious class methods.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
@@ -256,6 +256,131 @@ def testCoreTree_Methods(dummyGUI, dummyItems):
# END Test testCoreTree_Methods
@pytest.mark.core
def testCoreTree_UpdateItemLayout(dummyGUI, dummyItems):
"""Test building a project tree from a list of items.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
# Check rejected items
assert not theTree.updateItemLayout("0000000000000", "H1") # Non-existent handle
assert not theTree.updateItemLayout("a000000000004", "H2") # Character file
assert not theTree.updateItemLayout("c000000000002", "H0") # Wrong header level
cHandle = "c000000000002"
# Check layouts we won't change
theTree[cHandle].setLayout(nwItemLayout.NO_LAYOUT)
assert not theTree.updateItemLayout("c000000000002", "H1")
theTree[cHandle].setLayout(nwItemLayout.TITLE)
assert not theTree.updateItemLayout("c000000000002", "H1")
theTree[cHandle].setLayout(nwItemLayout.PAGE)
assert not theTree.updateItemLayout("c000000000002", "H1")
theTree[cHandle].setLayout(nwItemLayout.NOTE)
assert not theTree.updateItemLayout("c000000000002", "H1")
# BOOK is also a layout we change to, but never from
theTree[cHandle].setLayout(nwItemLayout.BOOK)
assert not theTree.updateItemLayout("c000000000002", "H1")
# Test SCENE Changes
# ==================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert theTree.updateItemLayout("c000000000002", "H1")
assert theTree[cHandle].itemLayout == nwItemLayout.BOOK
# H2 -> CHAPTER
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert theTree.updateItemLayout("c000000000002", "H2")
assert theTree[cHandle].itemLayout == nwItemLayout.CHAPTER
# H3 -> No CHange
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert not theTree.updateItemLayout("c000000000002", "H3")
# H4 -> No CHange
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert not theTree.updateItemLayout("c000000000002", "H4")
# Test CHAPTER Changes
# ====================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert theTree.updateItemLayout("c000000000002", "H1")
assert theTree[cHandle].itemLayout == nwItemLayout.BOOK
# H2 -> No Change
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert not theTree.updateItemLayout("c000000000002", "H2")
# H3 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert theTree.updateItemLayout("c000000000002", "H3")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# H4 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert theTree.updateItemLayout("c000000000002", "H4")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# Test UNNUMBERED Changes
# =======================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert theTree.updateItemLayout("c000000000002", "H1")
assert theTree[cHandle].itemLayout == nwItemLayout.BOOK
# H2 -> No Change
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert not theTree.updateItemLayout("c000000000002", "H2")
# H3 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert theTree.updateItemLayout("c000000000002", "H3")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# H4 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert theTree.updateItemLayout("c000000000002", "H4")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# Test PARTITION Changes
# ======================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert not theTree.updateItemLayout("c000000000002", "H1")
# H2 -> No Change
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert theTree.updateItemLayout("c000000000002", "H2")
assert theTree[cHandle].itemLayout == nwItemLayout.CHAPTER
# H3 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert theTree.updateItemLayout("c000000000002", "H3")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# H4 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert theTree.updateItemLayout("c000000000002", "H4")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# END Test testCoreTree_UpdateItemLayout
@pytest.mark.core
def testCoreTree_MakeHandles(monkeypatch, dummyGUI):
"""Test generating item handles.
@@ -296,8 +421,8 @@ def testCoreTree_Stats(dummyGUI, dummyItems):
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
theTree._treeOrder.append("dummy")
@@ -323,9 +448,9 @@ def testCoreTree_Reorder(dummyGUI, dummyItems):
theTree = NWTree(theProject)
aHandle = []
for tHandle, pHande, nwItem in dummyItems:
for tHandle, pHandle, nwItem in dummyItems:
aHandle.append(tHandle)
theTree.append(tHandle, pHande, nwItem)
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
@@ -348,13 +473,13 @@ def testCoreTree_Reorder(dummyGUI, dummyItems):
@pytest.mark.core
def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems):
"""Test changing tree order.
"""Test packing and unpacking the tree to and from XML.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
@@ -408,8 +533,8 @@ def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir):
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
theTree._treeOrder.append("dummy")