Add support for reading and caching story keys

This commit is contained in:
Veronica Berglyd Olsen
2025-04-06 20:00:46 +02:00
parent edc19187f0
commit e2293e7c1e
4 changed files with 73 additions and 53 deletions
+20 -5
View File
@@ -893,6 +893,21 @@ class TagsIndex:
return
class IndexCache:
"""Core: Item Index Lookup Data Class
A small data class passed between all objects of the Item Index
which provides lookup capabilities and caching for shared data.
"""
__slots__ = ("tags", "story")
def __init__(self, tagsIndex: TagsIndex) -> None:
self.tags: TagsIndex = tagsIndex
self.story: set[str] = set()
return
# The Item Index Objects
# ======================
@@ -906,11 +921,11 @@ class ItemIndex:
IndexHeading object for each heading of the text.
"""
__slots__ = ("_project", "_tags", "_items")
__slots__ = ("_project", "_cache", "_items")
def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None:
self._project = project
self._tags = tagsIndex
self._cache = IndexCache(tagsIndex)
self._items: dict[str, IndexNode] = {}
return
@@ -937,7 +952,7 @@ class ItemIndex:
"""Add a new item to the index. This will overwrite the item if
it already exists.
"""
self._items[tHandle] = IndexNode(self._tags, tHandle, nwItem)
self._items[tHandle] = IndexNode(self._cache, tHandle, nwItem)
return
def allItemTags(self, tHandle: str) -> list[str]:
@@ -995,7 +1010,7 @@ class ItemIndex:
if tHandle in self._items:
tItem = self._items[tHandle]
sTitle = tItem.nextHeading()
tItem.addHeading(IndexHeading(self._tags, sTitle, lineNo, level, text))
tItem.addHeading(IndexHeading(self._cache, sTitle, lineNo, level, text))
return sTitle
return TT_NONE
@@ -1069,7 +1084,7 @@ class ItemIndex:
nwItem = self._project.tree[tHandle]
if nwItem is not None:
tItem = IndexNode(self._tags, tHandle, nwItem)
tItem = IndexNode(self._cache, tHandle, nwItem)
tItem.unpackData(tData)
self._items[tHandle] = tItem
+22 -17
View File
@@ -32,12 +32,12 @@ from collections.abc import ItemsView, Sequence
from typing import TYPE_CHECKING, Literal
from novelwriter import CONFIG
from novelwriter.common import checkInt, isListInstance, isTitleTag
from novelwriter.common import checkInt, compact, isListInstance, isTitleTag
from novelwriter.constants import nwKeyWords, nwStyles
from novelwriter.enum import nwComment
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.index import TagsIndex
from novelwriter.core.index import IndexCache
from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
@@ -58,13 +58,13 @@ class IndexNode:
must be reset each time the item is re-indexed.
"""
__slots__ = ("_tags", "_handle", "_item", "_headings", "_notes", "_count")
__slots__ = ("_cache", "_handle", "_item", "_headings", "_notes", "_count")
def __init__(self, tagsIndex: TagsIndex, tHandle: str, nwItem: NWItem) -> None:
self._tags = tagsIndex
def __init__(self, cache: IndexCache, tHandle: str, nwItem: NWItem) -> None:
self._cache = cache
self._handle = tHandle
self._item = nwItem
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(self._tags, TT_NONE)}
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(self._cache, TT_NONE)}
self._notes: dict[str, set[str]] = {}
self._count = 0
return
@@ -117,7 +117,7 @@ class IndexNode:
def setHeadingComment(self, sTitle: str, comment: nwComment, key: str, text: str) -> None:
"""Set the comment text of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setComment(comment, key, text)
self._headings[sTitle].setComment(comment.name, key, text)
return
def setHeadingTag(self, sTitle: str, tag: str) -> None:
@@ -182,7 +182,7 @@ class IndexNode:
"""Unpack an item entry from the data."""
for key, entry in data.items():
if isTitleTag(key):
heading = IndexHeading(self._tags, key)
heading = IndexHeading(self._cache, key)
heading.unpackData(entry)
self.addHeading(heading)
elif key == "document":
@@ -206,15 +206,15 @@ class IndexHeading:
"""
__slots__ = (
"_tags", "_key", "_line", "_level", "_title",
"_cache", "_key", "_line", "_level", "_title",
"_counts", "_tag", "_refs", "_comments",
)
def __init__(
self, tagsIndex: TagsIndex, key: str, line: int = 0,
self, cache: IndexCache, key: str, line: int = 0,
level: str = "H0", title: str = "",
) -> None:
self._tags = tagsIndex
self._cache = cache
self._key = key
self._line = line
self._level = level
@@ -303,10 +303,14 @@ class IndexHeading:
)
return
def setComment(self, comment: nwComment, key: str, text: str) -> None:
def setComment(self, comment: str, key: str, text: str) -> None:
"""Set the text for a comment and make sure it is a string."""
if comment in (nwComment.SHORT, nwComment.SYNOPSIS):
self._comments["summary"] = str(text)
match comment.lower():
case "short" | "synopsis" | "summary":
self._comments["summary"] = str(text)
case "story" if key:
self._cache.story.add(key)
self._comments[f"story.{key}"] = str(text)
return
def setTag(self, tag: str) -> None:
@@ -334,7 +338,7 @@ class IndexHeading:
refs = {x: [] for x in nwKeyWords.VALID_KEYS}
for tag, types in self._refs.items():
for keyword in types:
if keyword in refs and (name := self._tags.tagName(tag)):
if keyword in refs and (name := self._cache.tags.tagName(tag)):
refs[keyword].append(name)
return refs
@@ -342,7 +346,7 @@ class IndexHeading:
"""Extract all references for this heading."""
refs = []
for tag, types in self._refs.items():
if keyword in types and (name := self._tags.tagName(tag)):
if keyword in types and (name := self._cache.tags.tagName(tag)):
refs.append(name)
return refs
@@ -387,7 +391,8 @@ class IndexHeading:
else:
raise ValueError("Heading reference contains an invalid keyword")
elif key == "summary" or key.startswith("story"):
self._comments[str(key)] = str(entry)
comment, _, kind = str(key).partition(".")
self.setComment(comment, compact(kind), str(entry))
else:
raise KeyError("Unknown key in heading entry")
return
+29 -29
View File
@@ -23,7 +23,7 @@ from __future__ import annotations
import pytest
from novelwriter import CONFIG
from novelwriter.core.index import TagsIndex
from novelwriter.core.index import IndexCache, TagsIndex
from novelwriter.core.indexdata import IndexHeading, IndexNode
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
@@ -36,10 +36,10 @@ def testCoreIndexData_IndexNode(mockGUI):
handle = "0123456789abc"
project = NWProject()
item = NWItem(project, handle)
tags = TagsIndex()
cache = IndexCache(TagsIndex())
# Defaults
node = IndexNode(tags, handle, item)
node = IndexNode(cache, handle, item)
assert node.handle == handle
assert node.item is item
assert str(node) == f"<IndexNode handle='{handle}'>"
@@ -48,8 +48,8 @@ def testCoreIndexData_IndexNode(mockGUI):
assert "T0000" in node # Placeholder heading
# Add a heading
head1 = IndexHeading(tags, node.nextHeading(), line=1, level="H1", title="Heading 1")
head2 = IndexHeading(tags, node.nextHeading(), line=10, level="H2", title="Heading 2")
head1 = IndexHeading(cache, node.nextHeading(), line=1, level="H1", title="Heading 1")
head2 = IndexHeading(cache, node.nextHeading(), line=10, level="H2", title="Heading 2")
node.addHeading(head1)
node.addHeading(head2)
assert len(node) == 2
@@ -80,7 +80,7 @@ def testCoreIndexData_IndexNode(mockGUI):
assert head1.synopsis == "The first"
assert head2.synopsis == "The second"
# Set tags
# Set cache
node.setHeadingTag("T0001", "part1")
node.setHeadingTag("T0002", "part2")
assert head1.tag == "part1"
@@ -109,12 +109,12 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
handle = "0123456789abc"
project = NWProject()
item = NWItem(project, handle)
tags = TagsIndex()
node = IndexNode(tags, handle, item)
cache = IndexCache(TagsIndex())
node = IndexNode(cache, handle, item)
# Add some headings and notes
head1 = IndexHeading(tags, node.nextHeading(), line=1, level="H1", title="Heading 1")
head2 = IndexHeading(tags, node.nextHeading(), line=10, level="H2", title="Heading 2")
head1 = IndexHeading(cache, node.nextHeading(), line=1, level="H1", title="Heading 1")
head2 = IndexHeading(cache, node.nextHeading(), line=10, level="H2", title="Heading 2")
node.addHeading(head1)
node.addHeading(head2)
node.setHeadingCounts("T0001", 42, 13, 3)
@@ -133,7 +133,7 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
assert set(data["document"]["footnotes"]) == {"key1", "key2"}
# Create a new node
new = IndexNode(tags, handle, item)
new = IndexNode(cache, handle, item)
# Unpack heading one
data = {"T0001": {"meta": {
@@ -170,8 +170,8 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
def testCoreIndexData_IndexHeading():
"""Test the IndexHeading class."""
# Defaults
tags = TagsIndex()
head = IndexHeading(tags, "T0001")
cache = IndexCache(TagsIndex())
head = IndexHeading(cache, "T0001")
assert str(head) == "<IndexHeading key='T0001'>"
assert repr(head) == "<IndexHeading key='T0001'>"
assert head.key == "T0001"
@@ -214,7 +214,7 @@ def testCoreIndexData_IndexHeading():
assert head.mainCount == 42
# Set Summary
head.setComment(nwComment.SYNOPSIS, "", "In the beginning ...")
head.setComment(nwComment.SYNOPSIS.name, "", "In the beginning ...")
assert head.synopsis == "In the beginning ..."
# Set Tag
@@ -246,8 +246,8 @@ def testCoreIndexData_IndexHeading():
@pytest.mark.core
def testCoreIndexData_IndexHeadingReferences():
"""Test the IndexHeading references handling."""
tags = TagsIndex()
head = IndexHeading(tags, "T0001")
cache = IndexCache(TagsIndex())
head = IndexHeading(cache, "T0001")
# Add some references
head.addReference("Jane", "@pov")
@@ -273,10 +273,10 @@ def testCoreIndexData_IndexHeadingReferences():
}
# Set names
tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER")
tags.add("John", "John", "0000000000000", "T00001", "CHARACTER")
tags.add("Main", "Main", "0000000000000", "T00001", "PLOT")
tags.add("Gun", "Gun", "0000000000000", "T00001", "OBJECT")
cache.tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER")
cache.tags.add("John", "John", "0000000000000", "T00001", "CHARACTER")
cache.tags.add("Main", "Main", "0000000000000", "T00001", "PLOT")
cache.tags.add("Gun", "Gun", "0000000000000", "T00001", "OBJECT")
# Now they should be populated
assert head.getReferences() == {
@@ -312,13 +312,13 @@ def testCoreIndexData_IndexHeadingReferences():
@pytest.mark.core
def testCoreIndexData_IndexHeadingUnpackMeta():
"""Test IndexHeading class meta unpacking."""
tags = TagsIndex()
cache = IndexCache(TagsIndex())
# Valid
data = {"meta": {
"level": "H1", "title": "So it Begins", "line": 1, "tag": "begins", "counts": [95, 18, 1]
}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
head.unpackData(data)
assert head.level == "H1"
assert head.title == "So it Begins"
@@ -332,7 +332,7 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
data = {"meta": {
"level": "H9", "title": None, "line": None, "tag": None, "counts": [42]
}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
head.unpackData(data)
assert head.level == "H0"
assert head.title == "None"
@@ -344,7 +344,7 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
# Empty
data = {"meta": {}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
head.unpackData(data)
assert head.level == "H0"
assert head.title == ""
@@ -358,13 +358,13 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
@pytest.mark.core
def testCoreIndexData_IndexHeadingUnpackRefs():
"""Test IndexHeading class refs unpacking."""
tags = TagsIndex()
cache = IndexCache(TagsIndex())
# Valid
data = {"refs": {
"jane": "@char,@pov", "john": "@char", "earth": "@location", "space": "@mention,@location"
}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
head.unpackData(data)
assert head.references["jane"] == {"@char", "@pov"}
assert head.references["john"] == {"@char"}
@@ -373,18 +373,18 @@ def testCoreIndexData_IndexHeadingUnpackRefs():
# Invalid key
data = {"refs": {0: "@char,@pov"}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
with pytest.raises(ValueError, match="Heading reference key must be a string"):
head.unpackData(data)
# Invalid value
data = {"refs": {"jane": None}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
with pytest.raises(ValueError, match="Heading reference value must be a string"):
head.unpackData(data)
# Invalid keyword
data = {"refs": {"jane": "@char,@pov,@stuff"}}
head = IndexHeading(tags, "T0001")
head = IndexHeading(cache, "T0001")
with pytest.raises(ValueError, match="Heading reference contains an invalid keyword"):
head.unpackData(data)
+2 -2
View File
@@ -156,8 +156,8 @@ def testCoreNovelModel_Data(nwGUI, fncPath, mockRnd):
model.append(scene)
# Add headings to scene
scene.addHeading(IndexHeading(scene._tags, "T0002", 10, "H4", "A Section"))
scene.addHeading(IndexHeading(scene._tags, "T0003", 10, "H4", "Another Section"))
scene.addHeading(IndexHeading(scene._cache, "T0002", 10, "H4", "A Section"))
scene.addHeading(IndexHeading(scene._cache, "T0003", 10, "H4", "Another Section"))
assert model.refresh(scene) is True
assert [
model.data(model.createIndex(i, 0), Qt.ItemDataRole.DisplayRole)