Add story structure comment parsing (#2284)
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"Short Description": "Short Description",
|
"Short Description": "Short Description",
|
||||||
"Footnotes": "Footnotes",
|
"Footnotes": "Footnotes",
|
||||||
"Comment": "Comment",
|
"Comment": "Comment",
|
||||||
|
"Story Structure": "Story Structure",
|
||||||
"Notes": "Notes",
|
"Notes": "Notes",
|
||||||
"Tag": "Tag",
|
"Tag": "Tag",
|
||||||
"Point of View": "Point of View",
|
"Point of View": "Point of View",
|
||||||
|
|||||||
+29
-11
@@ -387,10 +387,10 @@ class Index:
|
|||||||
|
|
||||||
elif line.startswith("%"):
|
elif line.startswith("%"):
|
||||||
cStyle, cKey, cText, _, _ = processComment(line)
|
cStyle, cKey, cText, _, _ = processComment(line)
|
||||||
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
|
if cStyle == nwComment.FOOTNOTE:
|
||||||
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
|
|
||||||
elif cStyle == nwComment.FOOTNOTE:
|
|
||||||
self._itemIndex.addNoteKey(tHandle, "footnotes", cKey)
|
self._itemIndex.addNoteKey(tHandle, "footnotes", cKey)
|
||||||
|
else:
|
||||||
|
self._itemIndex.setHeadingComment(tHandle, cTitle, cStyle, cKey, cText)
|
||||||
|
|
||||||
# Count words for remaining text after last heading
|
# Count words for remaining text after last heading
|
||||||
if pTitle != TT_NONE:
|
if pTitle != TT_NONE:
|
||||||
@@ -894,6 +894,21 @@ class TagsIndex:
|
|||||||
return
|
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
|
# The Item Index Objects
|
||||||
# ======================
|
# ======================
|
||||||
|
|
||||||
@@ -907,11 +922,11 @@ class ItemIndex:
|
|||||||
IndexHeading object for each heading of the text.
|
IndexHeading object for each heading of the text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_items", "_project", "_tags")
|
__slots__ = ("_cache", "_items", "_project")
|
||||||
|
|
||||||
def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None:
|
def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None:
|
||||||
self._project = project
|
self._project = project
|
||||||
self._tags = tagsIndex
|
self._cache = IndexCache(tagsIndex)
|
||||||
self._items: dict[str, IndexNode] = {}
|
self._items: dict[str, IndexNode] = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -938,7 +953,7 @@ class ItemIndex:
|
|||||||
"""Add a new item to the index. This will overwrite the item if
|
"""Add a new item to the index. This will overwrite the item if
|
||||||
it already exists.
|
it already exists.
|
||||||
"""
|
"""
|
||||||
self._items[tHandle] = IndexNode(self._tags, tHandle, nwItem)
|
self._items[tHandle] = IndexNode(self._cache, tHandle, nwItem)
|
||||||
return
|
return
|
||||||
|
|
||||||
def allItemTags(self, tHandle: str) -> list[str]:
|
def allItemTags(self, tHandle: str) -> list[str]:
|
||||||
@@ -996,7 +1011,7 @@ class ItemIndex:
|
|||||||
if tHandle in self._items:
|
if tHandle in self._items:
|
||||||
tItem = self._items[tHandle]
|
tItem = self._items[tHandle]
|
||||||
sTitle = tItem.nextHeading()
|
sTitle = tItem.nextHeading()
|
||||||
tItem.addHeading(IndexHeading(self._tags, sTitle, lineNo, level, text))
|
tItem.addHeading(IndexHeading(self._cache, sTitle, lineNo, level, text))
|
||||||
return sTitle
|
return sTitle
|
||||||
return TT_NONE
|
return TT_NONE
|
||||||
|
|
||||||
@@ -1008,10 +1023,13 @@ class ItemIndex:
|
|||||||
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
|
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setHeadingSynopsis(self, tHandle: str, sTitle: str, text: str) -> None:
|
def setHeadingComment(
|
||||||
"""Set the synopsis text for a heading on a given item."""
|
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:
|
if tHandle in self._items:
|
||||||
self._items[tHandle].setHeadingSynopsis(sTitle, text)
|
self._items[tHandle].setHeadingComment(sTitle, comment, key, text)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
|
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
|
||||||
@@ -1067,7 +1085,7 @@ class ItemIndex:
|
|||||||
|
|
||||||
nwItem = self._project.tree[tHandle]
|
nwItem = self._project.tree[tHandle]
|
||||||
if nwItem is not None:
|
if nwItem is not None:
|
||||||
tItem = IndexNode(self._tags, tHandle, nwItem)
|
tItem = IndexNode(self._cache, tHandle, nwItem)
|
||||||
tItem.unpackData(tData)
|
tItem.unpackData(tData)
|
||||||
self._items[tHandle] = tItem
|
self._items[tHandle] = tItem
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ import logging
|
|||||||
from typing import TYPE_CHECKING, Literal
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
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.constants import nwKeyWords, nwStyles
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import ItemsView, Sequence
|
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.core.item import NWItem
|
||||||
|
from novelwriter.enum import nwComment
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -58,13 +59,13 @@ class IndexNode:
|
|||||||
must be reset each time the item is re-indexed.
|
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:
|
def __init__(self, cache: IndexCache, tHandle: str, nwItem: NWItem) -> None:
|
||||||
self._tags = tagsIndex
|
self._cache = cache
|
||||||
self._handle = tHandle
|
self._handle = tHandle
|
||||||
self._item = nwItem
|
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._notes: dict[str, set[str]] = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
return
|
return
|
||||||
@@ -114,10 +115,10 @@ class IndexNode:
|
|||||||
self._headings[sTitle].setCounts([cCount, wCount, pCount])
|
self._headings[sTitle].setCounts([cCount, wCount, pCount])
|
||||||
return
|
return
|
||||||
|
|
||||||
def setHeadingSynopsis(self, sTitle: str, text: str) -> None:
|
def setHeadingComment(self, sTitle: str, comment: nwComment, key: str, text: str) -> None:
|
||||||
"""Set the synopsis text of a heading."""
|
"""Set the comment text of a heading."""
|
||||||
if sTitle in self._headings:
|
if sTitle in self._headings:
|
||||||
self._headings[sTitle].setSynopsis(text)
|
self._headings[sTitle].setComment(comment.name, key, text)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setHeadingTag(self, sTitle: str, tag: str) -> None:
|
def setHeadingTag(self, sTitle: str, tag: str) -> None:
|
||||||
@@ -182,7 +183,7 @@ class IndexNode:
|
|||||||
"""Unpack an item entry from the data."""
|
"""Unpack an item entry from the data."""
|
||||||
for key, entry in data.items():
|
for key, entry in data.items():
|
||||||
if isTitleTag(key):
|
if isTitleTag(key):
|
||||||
heading = IndexHeading(self._tags, key)
|
heading = IndexHeading(self._cache, key)
|
||||||
heading.unpackData(entry)
|
heading.unpackData(entry)
|
||||||
self.addHeading(heading)
|
self.addHeading(heading)
|
||||||
elif key == "document":
|
elif key == "document":
|
||||||
@@ -206,15 +207,15 @@ class IndexHeading:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_comments", "_counts", "_key", "_level", "_line", "_refs", "_tag",
|
"_cache", "_comments", "_counts", "_key", "_level", "_line", "_refs",
|
||||||
"_tags", "_title",
|
"_tag", "_title",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, tagsIndex: TagsIndex, key: str, line: int = 0,
|
self, cache: IndexCache, key: str, line: int = 0,
|
||||||
level: str = "H0", title: str = "",
|
level: str = "H0", title: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
self._tags = tagsIndex
|
self._cache = cache
|
||||||
self._key = key
|
self._key = key
|
||||||
self._line = line
|
self._line = line
|
||||||
self._level = level
|
self._level = level
|
||||||
@@ -268,6 +269,10 @@ class IndexHeading:
|
|||||||
def synopsis(self) -> str:
|
def synopsis(self) -> str:
|
||||||
return self._comments.get("summary", "")
|
return self._comments.get("summary", "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments(self) -> dict[str, str]:
|
||||||
|
return self._comments
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tag(self) -> str:
|
def tag(self) -> str:
|
||||||
return self._tag
|
return self._tag
|
||||||
@@ -303,9 +308,14 @@ class IndexHeading:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setSynopsis(self, text: str) -> None:
|
def setComment(self, comment: str, key: str, text: str) -> None:
|
||||||
"""Set the synopsis text and make sure it is a string."""
|
"""Set the text for a comment and make sure it is a string."""
|
||||||
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
|
return
|
||||||
|
|
||||||
def setTag(self, tag: str) -> None:
|
def setTag(self, tag: str) -> None:
|
||||||
@@ -333,7 +343,7 @@ class IndexHeading:
|
|||||||
refs = {x: [] for x in nwKeyWords.VALID_KEYS}
|
refs = {x: [] for x in nwKeyWords.VALID_KEYS}
|
||||||
for tag, types in self._refs.items():
|
for tag, types in self._refs.items():
|
||||||
for keyword in types:
|
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)
|
refs[keyword].append(name)
|
||||||
return refs
|
return refs
|
||||||
|
|
||||||
@@ -341,7 +351,7 @@ class IndexHeading:
|
|||||||
"""Extract all references for this heading."""
|
"""Extract all references for this heading."""
|
||||||
refs = []
|
refs = []
|
||||||
for tag, types in self._refs.items():
|
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)
|
refs.append(name)
|
||||||
return refs
|
return refs
|
||||||
|
|
||||||
@@ -386,7 +396,8 @@ class IndexHeading:
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Heading reference contains an invalid keyword")
|
raise ValueError("Heading reference contains an invalid keyword")
|
||||||
elif key == "summary" or key.startswith("story"):
|
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:
|
else:
|
||||||
raise KeyError("Unknown key in heading entry")
|
raise KeyError("Unknown key in heading entry")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ class ComStyle(NamedTuple):
|
|||||||
COMMENT_STYLE = {
|
COMMENT_STYLE = {
|
||||||
nwComment.PLAIN: ComStyle("Comment", "comment", "comment"),
|
nwComment.PLAIN: ComStyle("Comment", "comment", "comment"),
|
||||||
nwComment.IGNORE: ComStyle(),
|
nwComment.IGNORE: ComStyle(),
|
||||||
nwComment.SYNOPSIS: ComStyle("Synopsis", "modifier", "synopsis"),
|
nwComment.SYNOPSIS: ComStyle("Synopsis", "modifier", "note"),
|
||||||
nwComment.SHORT: ComStyle("Short Description", "modifier", "synopsis"),
|
nwComment.SHORT: ComStyle("Short Description", "modifier", "note"),
|
||||||
nwComment.NOTE: ComStyle("Note", "modifier", "note"),
|
nwComment.NOTE: ComStyle("Note", "modifier", "note"),
|
||||||
nwComment.FOOTNOTE: ComStyle("", "modifier", "note"),
|
nwComment.FOOTNOTE: ComStyle("", "modifier", "note"),
|
||||||
nwComment.COMMENT: ComStyle(),
|
nwComment.COMMENT: ComStyle(),
|
||||||
nwComment.STORY: ComStyle("", "modifier", "note"),
|
nwComment.STORY: ComStyle("Story Structure", "modifier", "note"),
|
||||||
}
|
}
|
||||||
HEADINGS = [
|
HEADINGS = [
|
||||||
BlockTyp.TITLE, BlockTyp.PART, BlockTyp.HEAD1,
|
BlockTyp.TITLE, BlockTyp.PART, BlockTyp.HEAD1,
|
||||||
@@ -440,7 +440,7 @@ class Tokenizer(ABC):
|
|||||||
def initDocument(self) -> None:
|
def initDocument(self) -> None:
|
||||||
"""Initialise data after settings."""
|
"""Initialise data after settings."""
|
||||||
self._classes["modifier"] = self._theme.modifier
|
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["comment"] = self._theme.comment
|
||||||
self._classes["dialog"] = self._theme.dialog
|
self._classes["dialog"] = self._theme.dialog
|
||||||
self._classes["altdialog"] = self._theme.altdialog
|
self._classes["altdialog"] = self._theme.altdialog
|
||||||
@@ -615,7 +615,9 @@ class Tokenizer(ABC):
|
|||||||
if doJustify and not tStyle & BlockFmt.ALIGNED:
|
if doJustify and not tStyle & BlockFmt.ALIGNED:
|
||||||
tStyle |= BlockFmt.JUSTIFY
|
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]
|
bStyle = COMMENT_STYLE[cStyle]
|
||||||
tLine, tFmt = self._formatComment(bStyle, cKey, cText)
|
tLine, tFmt = self._formatComment(bStyle, cKey, cText)
|
||||||
tBlocks.append((
|
tBlocks.append((
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ MODIFIERS = {
|
|||||||
"short": nwComment.SHORT,
|
"short": nwComment.SHORT,
|
||||||
"note": nwComment.NOTE,
|
"note": nwComment.NOTE,
|
||||||
"footnote": nwComment.FOOTNOTE,
|
"footnote": nwComment.FOOTNOTE,
|
||||||
|
"story": nwComment.STORY,
|
||||||
}
|
}
|
||||||
KEY_REQ = {
|
KEY_REQ = {
|
||||||
"synopsis": 0, # Key not allowed
|
"synopsis": 0, # Key not allowed
|
||||||
"short": 0, # Key not allowed
|
"short": 0, # Key not allowed
|
||||||
"note": 1, # Key optional
|
"note": 1, # Key optional
|
||||||
"footnote": 2, # Key required
|
"footnote": 2, # Key required
|
||||||
|
"story": 2, # Key required
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
%%~name: Making a Scene
|
%%~name: Making a Scene
|
||||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: 8d245fa740926779d19741ff7f75ef387b55130c
|
%%~hash: c057a5e9309b0e764c367b0fe9ab0607e3308622
|
||||||
%%~date: Unknown/2024-10-25 23:54:52
|
%%~date: Unknown/2025-04-08 20:10:33
|
||||||
### Making a Scene
|
### Making a Scene
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
@location: Earth
|
@location: Earth
|
||||||
@mention: Space
|
@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.
|
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.
|
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'?>
|
<?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">
|
<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="2166" autoCount="282" editTime="96058">
|
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2176" autoCount="285" editTime="96579">
|
||||||
<name>Sample Project</name>
|
<name>Sample Project</name>
|
||||||
<author>Jane Smith</author>
|
<author>Jane Smith</author>
|
||||||
</project>
|
</project>
|
||||||
@@ -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="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>
|
<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">
|
||||||
|
|||||||
@@ -1196,7 +1196,7 @@ def testCoreIndex_ItemIndex(nwGUI, fncPath, mockRnd):
|
|||||||
|
|
||||||
# Set the remaining data values
|
# Set the remaining data values
|
||||||
itemIndex.setHeadingCounts(cHandle, "T0001", 60, 10, 2)
|
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.setHeadingTag(cHandle, "T0001", "One")
|
||||||
itemIndex.addHeadingRef(cHandle, "T0001", ["Jane"], "@pov")
|
itemIndex.addHeadingRef(cHandle, "T0001", ["Jane"], "@pov")
|
||||||
itemIndex.addHeadingRef(cHandle, "T0001", ["Jane"], "@focus")
|
itemIndex.addHeadingRef(cHandle, "T0001", ["Jane"], "@focus")
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
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.indexdata import IndexHeading, IndexNode
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
|
from novelwriter.enum import nwComment
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
@@ -35,10 +36,10 @@ def testCoreIndexData_IndexNode(mockGUI):
|
|||||||
handle = "0123456789abc"
|
handle = "0123456789abc"
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
item = NWItem(project, handle)
|
item = NWItem(project, handle)
|
||||||
tags = TagsIndex()
|
cache = IndexCache(TagsIndex())
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
node = IndexNode(tags, handle, item)
|
node = IndexNode(cache, handle, item)
|
||||||
assert node.handle == handle
|
assert node.handle == handle
|
||||||
assert node.item is item
|
assert node.item is item
|
||||||
assert str(node) == f"<IndexNode handle='{handle}'>"
|
assert str(node) == f"<IndexNode handle='{handle}'>"
|
||||||
@@ -47,8 +48,8 @@ def testCoreIndexData_IndexNode(mockGUI):
|
|||||||
assert "T0000" in node # Placeholder heading
|
assert "T0000" in node # Placeholder heading
|
||||||
|
|
||||||
# Add a heading
|
# Add a heading
|
||||||
head1 = IndexHeading(tags, node.nextHeading(), line=1, level="H1", title="Heading 1")
|
head1 = IndexHeading(cache, node.nextHeading(), line=1, level="H1", title="Heading 1")
|
||||||
head2 = IndexHeading(tags, node.nextHeading(), line=10, level="H2", title="Heading 2")
|
head2 = IndexHeading(cache, node.nextHeading(), line=10, level="H2", title="Heading 2")
|
||||||
node.addHeading(head1)
|
node.addHeading(head1)
|
||||||
node.addHeading(head2)
|
node.addHeading(head2)
|
||||||
assert len(node) == 2
|
assert len(node) == 2
|
||||||
@@ -74,12 +75,12 @@ def testCoreIndexData_IndexNode(mockGUI):
|
|||||||
assert head2.paraCount == 6
|
assert head2.paraCount == 6
|
||||||
|
|
||||||
# Set synopsis
|
# Set synopsis
|
||||||
node.setHeadingSynopsis("T0001", "The first")
|
node.setHeadingComment("T0001", nwComment.SYNOPSIS, "", "The first")
|
||||||
node.setHeadingSynopsis("T0002", "The second")
|
node.setHeadingComment("T0002", nwComment.SYNOPSIS, "", "The second")
|
||||||
assert head1.synopsis == "The first"
|
assert head1.synopsis == "The first"
|
||||||
assert head2.synopsis == "The second"
|
assert head2.synopsis == "The second"
|
||||||
|
|
||||||
# Set tags
|
# Set cache
|
||||||
node.setHeadingTag("T0001", "part1")
|
node.setHeadingTag("T0001", "part1")
|
||||||
node.setHeadingTag("T0002", "part2")
|
node.setHeadingTag("T0002", "part2")
|
||||||
assert head1.tag == "part1"
|
assert head1.tag == "part1"
|
||||||
@@ -108,12 +109,12 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
|
|||||||
handle = "0123456789abc"
|
handle = "0123456789abc"
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
item = NWItem(project, handle)
|
item = NWItem(project, handle)
|
||||||
tags = TagsIndex()
|
cache = IndexCache(TagsIndex())
|
||||||
node = IndexNode(tags, handle, item)
|
node = IndexNode(cache, handle, item)
|
||||||
|
|
||||||
# Add some headings and notes
|
# Add some headings and notes
|
||||||
head1 = IndexHeading(tags, node.nextHeading(), line=1, level="H1", title="Heading 1")
|
head1 = IndexHeading(cache, node.nextHeading(), line=1, level="H1", title="Heading 1")
|
||||||
head2 = IndexHeading(tags, node.nextHeading(), line=10, level="H2", title="Heading 2")
|
head2 = IndexHeading(cache, node.nextHeading(), line=10, level="H2", title="Heading 2")
|
||||||
node.addHeading(head1)
|
node.addHeading(head1)
|
||||||
node.addHeading(head2)
|
node.addHeading(head2)
|
||||||
node.setHeadingCounts("T0001", 42, 13, 3)
|
node.setHeadingCounts("T0001", 42, 13, 3)
|
||||||
@@ -132,7 +133,7 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
|
|||||||
assert set(data["document"]["footnotes"]) == {"key1", "key2"}
|
assert set(data["document"]["footnotes"]) == {"key1", "key2"}
|
||||||
|
|
||||||
# Create a new node
|
# Create a new node
|
||||||
new = IndexNode(tags, handle, item)
|
new = IndexNode(cache, handle, item)
|
||||||
|
|
||||||
# Unpack heading one
|
# Unpack heading one
|
||||||
data = {"T0001": {"meta": {
|
data = {"T0001": {"meta": {
|
||||||
@@ -169,8 +170,8 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
|
|||||||
def testCoreIndexData_IndexHeading():
|
def testCoreIndexData_IndexHeading():
|
||||||
"""Test the IndexHeading class."""
|
"""Test the IndexHeading class."""
|
||||||
# Defaults
|
# Defaults
|
||||||
tags = TagsIndex()
|
cache = IndexCache(TagsIndex())
|
||||||
head = IndexHeading(tags, "T0001")
|
head = IndexHeading(cache, "T0001")
|
||||||
assert str(head) == "<IndexHeading key='T0001'>"
|
assert str(head) == "<IndexHeading key='T0001'>"
|
||||||
assert repr(head) == "<IndexHeading key='T0001'>"
|
assert repr(head) == "<IndexHeading key='T0001'>"
|
||||||
assert head.key == "T0001"
|
assert head.key == "T0001"
|
||||||
@@ -213,9 +214,16 @@ def testCoreIndexData_IndexHeading():
|
|||||||
assert head.mainCount == 42
|
assert head.mainCount == 42
|
||||||
|
|
||||||
# Set Summary
|
# Set Summary
|
||||||
head.setSynopsis("In the beginning ...")
|
head.setComment(nwComment.SYNOPSIS.name, "", "In the beginning ...")
|
||||||
assert head.synopsis == "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
|
# Set Tag
|
||||||
head.setTag("Stuff")
|
head.setTag("Stuff")
|
||||||
assert head.tag == "stuff" # Case insensitive
|
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)},
|
"meta": {"level": "H1", "title": "", "line": 42, "tag": "stuff", "counts": (42, 4, 2)},
|
||||||
"refs": {"stuff": "@object"},
|
"refs": {"stuff": "@object"},
|
||||||
"summary": "In the beginning ...",
|
"summary": "In the beginning ...",
|
||||||
|
"story.crisis": "It exploded!",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Unpack KeyError
|
# Unpack KeyError
|
||||||
@@ -245,8 +254,8 @@ def testCoreIndexData_IndexHeading():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreIndexData_IndexHeadingReferences():
|
def testCoreIndexData_IndexHeadingReferences():
|
||||||
"""Test the IndexHeading references handling."""
|
"""Test the IndexHeading references handling."""
|
||||||
tags = TagsIndex()
|
cache = IndexCache(TagsIndex())
|
||||||
head = IndexHeading(tags, "T0001")
|
head = IndexHeading(cache, "T0001")
|
||||||
|
|
||||||
# Add some references
|
# Add some references
|
||||||
head.addReference("Jane", "@pov")
|
head.addReference("Jane", "@pov")
|
||||||
@@ -272,10 +281,10 @@ def testCoreIndexData_IndexHeadingReferences():
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Set names
|
# Set names
|
||||||
tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER")
|
cache.tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER")
|
||||||
tags.add("John", "John", "0000000000000", "T00001", "CHARACTER")
|
cache.tags.add("John", "John", "0000000000000", "T00001", "CHARACTER")
|
||||||
tags.add("Main", "Main", "0000000000000", "T00001", "PLOT")
|
cache.tags.add("Main", "Main", "0000000000000", "T00001", "PLOT")
|
||||||
tags.add("Gun", "Gun", "0000000000000", "T00001", "OBJECT")
|
cache.tags.add("Gun", "Gun", "0000000000000", "T00001", "OBJECT")
|
||||||
|
|
||||||
# Now they should be populated
|
# Now they should be populated
|
||||||
assert head.getReferences() == {
|
assert head.getReferences() == {
|
||||||
@@ -311,13 +320,13 @@ def testCoreIndexData_IndexHeadingReferences():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreIndexData_IndexHeadingUnpackMeta():
|
def testCoreIndexData_IndexHeadingUnpackMeta():
|
||||||
"""Test IndexHeading class meta unpacking."""
|
"""Test IndexHeading class meta unpacking."""
|
||||||
tags = TagsIndex()
|
cache = IndexCache(TagsIndex())
|
||||||
|
|
||||||
# Valid
|
# Valid
|
||||||
data = {"meta": {
|
data = {"meta": {
|
||||||
"level": "H1", "title": "So it Begins", "line": 1, "tag": "begins", "counts": [95, 18, 1]
|
"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)
|
head.unpackData(data)
|
||||||
assert head.level == "H1"
|
assert head.level == "H1"
|
||||||
assert head.title == "So it Begins"
|
assert head.title == "So it Begins"
|
||||||
@@ -331,7 +340,7 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
|
|||||||
data = {"meta": {
|
data = {"meta": {
|
||||||
"level": "H9", "title": None, "line": None, "tag": None, "counts": [42]
|
"level": "H9", "title": None, "line": None, "tag": None, "counts": [42]
|
||||||
}}
|
}}
|
||||||
head = IndexHeading(tags, "T0001")
|
head = IndexHeading(cache, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.level == "H0"
|
assert head.level == "H0"
|
||||||
assert head.title == "None"
|
assert head.title == "None"
|
||||||
@@ -343,7 +352,7 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
|
|||||||
|
|
||||||
# Empty
|
# Empty
|
||||||
data = {"meta": {}}
|
data = {"meta": {}}
|
||||||
head = IndexHeading(tags, "T0001")
|
head = IndexHeading(cache, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.level == "H0"
|
assert head.level == "H0"
|
||||||
assert head.title == ""
|
assert head.title == ""
|
||||||
@@ -357,13 +366,13 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreIndexData_IndexHeadingUnpackRefs():
|
def testCoreIndexData_IndexHeadingUnpackRefs():
|
||||||
"""Test IndexHeading class refs unpacking."""
|
"""Test IndexHeading class refs unpacking."""
|
||||||
tags = TagsIndex()
|
cache = IndexCache(TagsIndex())
|
||||||
|
|
||||||
# Valid
|
# Valid
|
||||||
data = {"refs": {
|
data = {"refs": {
|
||||||
"jane": "@char,@pov", "john": "@char", "earth": "@location", "space": "@mention,@location"
|
"jane": "@char,@pov", "john": "@char", "earth": "@location", "space": "@mention,@location"
|
||||||
}}
|
}}
|
||||||
head = IndexHeading(tags, "T0001")
|
head = IndexHeading(cache, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.references["jane"] == {"@char", "@pov"}
|
assert head.references["jane"] == {"@char", "@pov"}
|
||||||
assert head.references["john"] == {"@char"}
|
assert head.references["john"] == {"@char"}
|
||||||
@@ -372,18 +381,18 @@ def testCoreIndexData_IndexHeadingUnpackRefs():
|
|||||||
|
|
||||||
# Invalid key
|
# Invalid key
|
||||||
data = {"refs": {0: "@char,@pov"}}
|
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"):
|
with pytest.raises(ValueError, match="Heading reference key must be a string"):
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
|
|
||||||
# Invalid value
|
# Invalid value
|
||||||
data = {"refs": {"jane": None}}
|
data = {"refs": {"jane": None}}
|
||||||
head = IndexHeading(tags, "T0001")
|
head = IndexHeading(cache, "T0001")
|
||||||
with pytest.raises(ValueError, match="Heading reference value must be a string"):
|
with pytest.raises(ValueError, match="Heading reference value must be a string"):
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
|
|
||||||
# Invalid keyword
|
# Invalid keyword
|
||||||
data = {"refs": {"jane": "@char,@pov,@stuff"}}
|
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"):
|
with pytest.raises(ValueError, match="Heading reference contains an invalid keyword"):
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ def testCoreNovelModel_Data(nwGUI, fncPath, mockRnd):
|
|||||||
model.append(scene)
|
model.append(scene)
|
||||||
|
|
||||||
# Add headings to scene
|
# Add headings to scene
|
||||||
scene.addHeading(IndexHeading(scene._tags, "T0002", 10, "H4", "A Section"))
|
scene.addHeading(IndexHeading(scene._cache, "T0002", 10, "H4", "A Section"))
|
||||||
scene.addHeading(IndexHeading(scene._tags, "T0003", 10, "H4", "Another Section"))
|
scene.addHeading(IndexHeading(scene._cache, "T0003", 10, "H4", "Another Section"))
|
||||||
assert model.refresh(scene) is True
|
assert model.refresh(scene) is True
|
||||||
assert [
|
assert [
|
||||||
model.data(model.createIndex(i, 0), Qt.ItemDataRole.DisplayRole)
|
model.data(model.createIndex(i, 0), Qt.ItemDataRole.DisplayRole)
|
||||||
|
|||||||
@@ -785,7 +785,7 @@ def testFmtToken_MetaFormat(mockGUI):
|
|||||||
BlockTyp.COMMENT, "", "Synopsis: The synopsis", [
|
BlockTyp.COMMENT, "", "Synopsis: The synopsis", [
|
||||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
||||||
(9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
|
(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
|
], BlockFmt.NONE
|
||||||
)]
|
)]
|
||||||
|
|
||||||
@@ -802,7 +802,7 @@ def testFmtToken_MetaFormat(mockGUI):
|
|||||||
BlockTyp.COMMENT, "", "Short Description: A short description", [
|
BlockTyp.COMMENT, "", "Short Description: A short description", [
|
||||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
||||||
(18, TextFmt.COL_E, ""), (18, TextFmt.B_E, ""),
|
(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
|
], BlockFmt.NONE
|
||||||
)]
|
)]
|
||||||
|
|
||||||
@@ -1553,7 +1553,7 @@ def testFmtToken_TextIndent(mockGUI):
|
|||||||
tokens.tokenizeText()
|
tokens.tokenizeText()
|
||||||
tFmt = [
|
tFmt = [
|
||||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), (9, TextFmt.COL_E, ""),
|
(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 == [
|
assert tokens._blocks == [
|
||||||
(BlockTyp.HEAD3, TM1, "Scene Two", [], BlockFmt.NONE),
|
(BlockTyp.HEAD3, TM1, "Scene Two", [], BlockFmt.NONE),
|
||||||
@@ -1814,7 +1814,7 @@ def testFmtToken_FormatComment(mockGUI):
|
|||||||
"Synopsis: Hello world!", [
|
"Synopsis: Hello world!", [
|
||||||
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
|
||||||
(9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
|
(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