Add story structure comment parsing (#2284)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
"Short Description": "Short Description",
|
||||
"Footnotes": "Footnotes",
|
||||
"Comment": "Comment",
|
||||
"Story Structure": "Story Structure",
|
||||
"Notes": "Notes",
|
||||
"Tag": "Tag",
|
||||
"Point of View": "Point of View",
|
||||
|
||||
+29
-11
@@ -387,10 +387,10 @@ class Index:
|
||||
|
||||
elif line.startswith("%"):
|
||||
cStyle, cKey, cText, _, _ = processComment(line)
|
||||
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
|
||||
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
|
||||
elif cStyle == nwComment.FOOTNOTE:
|
||||
if cStyle == nwComment.FOOTNOTE:
|
||||
self._itemIndex.addNoteKey(tHandle, "footnotes", cKey)
|
||||
else:
|
||||
self._itemIndex.setHeadingComment(tHandle, cTitle, cStyle, cKey, cText)
|
||||
|
||||
# Count words for remaining text after last heading
|
||||
if pTitle != TT_NONE:
|
||||
@@ -894,6 +894,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__ = ("story", "tags")
|
||||
|
||||
def __init__(self, tagsIndex: TagsIndex) -> None:
|
||||
self.tags: TagsIndex = tagsIndex
|
||||
self.story: set[str] = set()
|
||||
return
|
||||
|
||||
|
||||
# The Item Index Objects
|
||||
# ======================
|
||||
|
||||
@@ -907,11 +922,11 @@ class ItemIndex:
|
||||
IndexHeading object for each heading of the text.
|
||||
"""
|
||||
|
||||
__slots__ = ("_items", "_project", "_tags")
|
||||
__slots__ = ("_cache", "_items", "_project")
|
||||
|
||||
def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None:
|
||||
self._project = project
|
||||
self._tags = tagsIndex
|
||||
self._cache = IndexCache(tagsIndex)
|
||||
self._items: dict[str, IndexNode] = {}
|
||||
return
|
||||
|
||||
@@ -938,7 +953,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]:
|
||||
@@ -996,7 +1011,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
|
||||
|
||||
@@ -1008,10 +1023,13 @@ class ItemIndex:
|
||||
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
|
||||
return
|
||||
|
||||
def setHeadingSynopsis(self, tHandle: str, sTitle: str, text: str) -> None:
|
||||
"""Set the synopsis text for a heading on a given item."""
|
||||
def setHeadingComment(
|
||||
self, tHandle: str, sTitle: str,
|
||||
comment: nwComment, key: str, text: str,
|
||||
) -> None:
|
||||
"""Set a story comment for a heading on a given item."""
|
||||
if tHandle in self._items:
|
||||
self._items[tHandle].setHeadingSynopsis(sTitle, text)
|
||||
self._items[tHandle].setHeadingComment(sTitle, comment, key, text)
|
||||
return
|
||||
|
||||
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
|
||||
@@ -1067,7 +1085,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
|
||||
|
||||
|
||||
@@ -31,14 +31,15 @@ import logging
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import ItemsView, Sequence
|
||||
|
||||
from novelwriter.core.index import TagsIndex
|
||||
from novelwriter.core.index import IndexCache
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.enum import nwComment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,13 +59,13 @@ class IndexNode:
|
||||
must be reset each time the item is re-indexed.
|
||||
"""
|
||||
|
||||
__slots__ = ("_count", "_handle", "_headings", "_item", "_notes", "_tags")
|
||||
__slots__ = ("_cache", "_count", "_handle", "_headings", "_item", "_notes")
|
||||
|
||||
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
|
||||
@@ -114,10 +115,10 @@ class IndexNode:
|
||||
self._headings[sTitle].setCounts([cCount, wCount, pCount])
|
||||
return
|
||||
|
||||
def setHeadingSynopsis(self, sTitle: str, text: str) -> None:
|
||||
"""Set the synopsis text of a heading."""
|
||||
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].setSynopsis(text)
|
||||
self._headings[sTitle].setComment(comment.name, key, text)
|
||||
return
|
||||
|
||||
def setHeadingTag(self, sTitle: str, tag: str) -> None:
|
||||
@@ -182,7 +183,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 +207,15 @@ class IndexHeading:
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_comments", "_counts", "_key", "_level", "_line", "_refs", "_tag",
|
||||
"_tags", "_title",
|
||||
"_cache", "_comments", "_counts", "_key", "_level", "_line", "_refs",
|
||||
"_tag", "_title",
|
||||
)
|
||||
|
||||
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
|
||||
@@ -268,6 +269,10 @@ class IndexHeading:
|
||||
def synopsis(self) -> str:
|
||||
return self._comments.get("summary", "")
|
||||
|
||||
@property
|
||||
def comments(self) -> dict[str, str]:
|
||||
return self._comments
|
||||
|
||||
@property
|
||||
def tag(self) -> str:
|
||||
return self._tag
|
||||
@@ -303,9 +308,14 @@ class IndexHeading:
|
||||
)
|
||||
return
|
||||
|
||||
def setSynopsis(self, text: str) -> None:
|
||||
"""Set the synopsis text and make sure it is a string."""
|
||||
self._comments["summary"] = str(text)
|
||||
def setComment(self, comment: str, key: str, text: str) -> None:
|
||||
"""Set the text for a comment and make sure it is a string."""
|
||||
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:
|
||||
@@ -333,7 +343,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
|
||||
|
||||
@@ -341,7 +351,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
|
||||
|
||||
@@ -386,7 +396,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
|
||||
|
||||
@@ -64,12 +64,12 @@ class ComStyle(NamedTuple):
|
||||
COMMENT_STYLE = {
|
||||
nwComment.PLAIN: ComStyle("Comment", "comment", "comment"),
|
||||
nwComment.IGNORE: ComStyle(),
|
||||
nwComment.SYNOPSIS: ComStyle("Synopsis", "modifier", "synopsis"),
|
||||
nwComment.SHORT: ComStyle("Short Description", "modifier", "synopsis"),
|
||||
nwComment.SYNOPSIS: ComStyle("Synopsis", "modifier", "note"),
|
||||
nwComment.SHORT: ComStyle("Short Description", "modifier", "note"),
|
||||
nwComment.NOTE: ComStyle("Note", "modifier", "note"),
|
||||
nwComment.FOOTNOTE: ComStyle("", "modifier", "note"),
|
||||
nwComment.COMMENT: ComStyle(),
|
||||
nwComment.STORY: ComStyle("", "modifier", "note"),
|
||||
nwComment.STORY: ComStyle("Story Structure", "modifier", "note"),
|
||||
}
|
||||
HEADINGS = [
|
||||
BlockTyp.TITLE, BlockTyp.PART, BlockTyp.HEAD1,
|
||||
@@ -440,7 +440,7 @@ class Tokenizer(ABC):
|
||||
def initDocument(self) -> None:
|
||||
"""Initialise data after settings."""
|
||||
self._classes["modifier"] = self._theme.modifier
|
||||
self._classes["synopsis"] = self._theme.note
|
||||
self._classes["note"] = self._theme.note
|
||||
self._classes["comment"] = self._theme.comment
|
||||
self._classes["dialog"] = self._theme.dialog
|
||||
self._classes["altdialog"] = self._theme.altdialog
|
||||
@@ -615,7 +615,9 @@ class Tokenizer(ABC):
|
||||
if doJustify and not tStyle & BlockFmt.ALIGNED:
|
||||
tStyle |= BlockFmt.JUSTIFY
|
||||
|
||||
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN):
|
||||
if cStyle in (
|
||||
nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN, nwComment.STORY
|
||||
):
|
||||
bStyle = COMMENT_STYLE[cStyle]
|
||||
tLine, tFmt = self._formatComment(bStyle, cKey, cText)
|
||||
tBlocks.append((
|
||||
|
||||
@@ -31,12 +31,14 @@ MODIFIERS = {
|
||||
"short": nwComment.SHORT,
|
||||
"note": nwComment.NOTE,
|
||||
"footnote": nwComment.FOOTNOTE,
|
||||
"story": nwComment.STORY,
|
||||
}
|
||||
KEY_REQ = {
|
||||
"synopsis": 0, # Key not allowed
|
||||
"short": 0, # Key not allowed
|
||||
"note": 1, # Key optional
|
||||
"footnote": 2, # Key required
|
||||
"story": 2, # Key required
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
%%~name: Making a Scene
|
||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 8d245fa740926779d19741ff7f75ef387b55130c
|
||||
%%~date: Unknown/2024-10-25 23:54:52
|
||||
%%~hash: c057a5e9309b0e764c367b0fe9ab0607e3308622
|
||||
%%~date: Unknown/2025-04-08 20:10:33
|
||||
### Making a Scene
|
||||
|
||||
@pov: Jane
|
||||
@@ -10,6 +10,8 @@
|
||||
@location: Earth
|
||||
@mention: Space
|
||||
|
||||
%Story.Resolution: You can describe the scene structure with story comments.
|
||||
|
||||
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference.
|
||||
|
||||
Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesn’t show it correctly, the export tool will not either.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.7a3" hexVersion="0x020700a3" fileVersion="1.5" fileRevision="4" timeStamp="2025-03-23 22:26:31">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2166" autoCount="282" editTime="96058">
|
||||
<novelWriterXML appVersion="2.7a3" hexVersion="0x020700a3" fileVersion="1.5" fileRevision="4" timeStamp="2025-04-08 20:12:28">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2176" autoCount="285" editTime="96579">
|
||||
<name>Sample Project</name>
|
||||
<author>Jane Smith</author>
|
||||
</project>
|
||||
@@ -58,7 +58,7 @@
|
||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||
</item>
|
||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="1035" />
|
||||
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="159" />
|
||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
|
||||
@@ -1196,7 +1196,7 @@ def testCoreIndex_ItemIndex(nwGUI, fncPath, mockRnd):
|
||||
|
||||
# Set the remaining data values
|
||||
itemIndex.setHeadingCounts(cHandle, "T0001", 60, 10, 2)
|
||||
itemIndex.setHeadingSynopsis(cHandle, "T0001", "In the beginning ...")
|
||||
itemIndex.setHeadingComment(cHandle, "T0001", nwComment.SYNOPSIS, "", "In the beginning ...")
|
||||
itemIndex.setHeadingTag(cHandle, "T0001", "One")
|
||||
itemIndex.addHeadingRef(cHandle, "T0001", ["Jane"], "@pov")
|
||||
itemIndex.addHeadingRef(cHandle, "T0001", ["Jane"], "@focus")
|
||||
|
||||
@@ -23,10 +23,11 @@ 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
|
||||
from novelwriter.enum import nwComment
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -35,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}'>"
|
||||
@@ -47,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
|
||||
@@ -74,12 +75,12 @@ def testCoreIndexData_IndexNode(mockGUI):
|
||||
assert head2.paraCount == 6
|
||||
|
||||
# Set synopsis
|
||||
node.setHeadingSynopsis("T0001", "The first")
|
||||
node.setHeadingSynopsis("T0002", "The second")
|
||||
node.setHeadingComment("T0001", nwComment.SYNOPSIS, "", "The first")
|
||||
node.setHeadingComment("T0002", nwComment.SYNOPSIS, "", "The second")
|
||||
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"
|
||||
@@ -108,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)
|
||||
@@ -132,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": {
|
||||
@@ -169,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"
|
||||
@@ -213,9 +214,16 @@ def testCoreIndexData_IndexHeading():
|
||||
assert head.mainCount == 42
|
||||
|
||||
# Set Summary
|
||||
head.setSynopsis("In the beginning ...")
|
||||
head.setComment(nwComment.SYNOPSIS.name, "", "In the beginning ...")
|
||||
assert head.synopsis == "In the beginning ..."
|
||||
|
||||
# Set Story Structure Comment
|
||||
head.setComment(nwComment.STORY.name, "crisis", "It exploded!")
|
||||
assert head.comments == {
|
||||
"summary": "In the beginning ...",
|
||||
"story.crisis": "It exploded!",
|
||||
}
|
||||
|
||||
# Set Tag
|
||||
head.setTag("Stuff")
|
||||
assert head.tag == "stuff" # Case insensitive
|
||||
@@ -231,6 +239,7 @@ def testCoreIndexData_IndexHeading():
|
||||
"meta": {"level": "H1", "title": "", "line": 42, "tag": "stuff", "counts": (42, 4, 2)},
|
||||
"refs": {"stuff": "@object"},
|
||||
"summary": "In the beginning ...",
|
||||
"story.crisis": "It exploded!",
|
||||
}
|
||||
|
||||
# Unpack KeyError
|
||||
@@ -245,8 +254,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")
|
||||
@@ -272,10 +281,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() == {
|
||||
@@ -311,13 +320,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"
|
||||
@@ -331,7 +340,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"
|
||||
@@ -343,7 +352,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 == ""
|
||||
@@ -357,13 +366,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"}
|
||||
@@ -372,18 +381,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -785,7 +785,7 @@ def testFmtToken_MetaFormat(mockGUI):
|
||||
BlockTyp.COMMENT, "", "Synopsis: The synopsis", [
|
||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
||||
(9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
|
||||
(10, TextFmt.COL_B, "synopsis"), (22, TextFmt.COL_E, "")
|
||||
(10, TextFmt.COL_B, "note"), (22, TextFmt.COL_E, "")
|
||||
], BlockFmt.NONE
|
||||
)]
|
||||
|
||||
@@ -802,7 +802,7 @@ def testFmtToken_MetaFormat(mockGUI):
|
||||
BlockTyp.COMMENT, "", "Short Description: A short description", [
|
||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
||||
(18, TextFmt.COL_E, ""), (18, TextFmt.B_E, ""),
|
||||
(19, TextFmt.COL_B, "synopsis"), (38, TextFmt.COL_E, ""),
|
||||
(19, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, ""),
|
||||
], BlockFmt.NONE
|
||||
)]
|
||||
|
||||
@@ -1553,7 +1553,7 @@ def testFmtToken_TextIndent(mockGUI):
|
||||
tokens.tokenizeText()
|
||||
tFmt = [
|
||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), (9, TextFmt.COL_E, ""),
|
||||
(9, TextFmt.B_E, ""), (10, TextFmt.COL_B, "synopsis"), (24, TextFmt.COL_E, ""),
|
||||
(9, TextFmt.B_E, ""), (10, TextFmt.COL_B, "note"), (24, TextFmt.COL_E, ""),
|
||||
]
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.HEAD3, TM1, "Scene Two", [], BlockFmt.NONE),
|
||||
@@ -1814,7 +1814,7 @@ def testFmtToken_FormatComment(mockGUI):
|
||||
"Synopsis: Hello world!", [
|
||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
||||
(9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
|
||||
(10, TextFmt.COL_B, "synopsis"), (22, TextFmt.COL_E, ""),
|
||||
(10, TextFmt.COL_B, "note"), (22, TextFmt.COL_E, ""),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user