From edc19187f05e600b1534466c53930a924f719ea3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Apr 2025 19:13:37 +0200 Subject: [PATCH 1/4] Generalise heading comments --- novelwriter/core/index.py | 15 +++++++++------ novelwriter/core/indexdata.py | 14 ++++++++------ novelwriter/text/comments.py | 2 ++ tests/test_core/test_core_index.py | 2 +- tests/test_core/test_core_indexdata.py | 7 ++++--- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 1097294c..6935947b 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -386,10 +386,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: @@ -1007,10 +1007,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: diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py index 9aee57c3..4dbd1954 100644 --- a/novelwriter/core/indexdata.py +++ b/novelwriter/core/indexdata.py @@ -34,6 +34,7 @@ from typing import TYPE_CHECKING, Literal from novelwriter import CONFIG from novelwriter.common import checkInt, 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 @@ -113,10 +114,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, key, text) return def setHeadingTag(self, sTitle: str, tag: str) -> None: @@ -302,9 +303,10 @@ 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: nwComment, 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) return def setTag(self, tag: str) -> None: diff --git a/novelwriter/text/comments.py b/novelwriter/text/comments.py index d3e22454..d2a62738 100644 --- a/novelwriter/text/comments.py +++ b/novelwriter/text/comments.py @@ -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 } diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index d58fbf15..723f8f9f 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -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") diff --git a/tests/test_core/test_core_indexdata.py b/tests/test_core/test_core_indexdata.py index c4fbb3f9..85adc7eb 100644 --- a/tests/test_core/test_core_indexdata.py +++ b/tests/test_core/test_core_indexdata.py @@ -27,6 +27,7 @@ from novelwriter.core.index import 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 @@ -74,8 +75,8 @@ 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" @@ -213,7 +214,7 @@ def testCoreIndexData_IndexHeading(): assert head.mainCount == 42 # Set Summary - head.setSynopsis("In the beginning ...") + head.setComment(nwComment.SYNOPSIS, "", "In the beginning ...") assert head.synopsis == "In the beginning ..." # Set Tag From e2293e7c1eab286407d851cb1d4483c285055271 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Apr 2025 20:00:46 +0200 Subject: [PATCH 2/4] Add support for reading and caching story keys --- novelwriter/core/index.py | 25 ++++++++--- novelwriter/core/indexdata.py | 39 +++++++++-------- tests/test_core/test_core_indexdata.py | 58 ++++++++++++------------- tests/test_core/test_core_novelmodel.py | 4 +- 4 files changed, 73 insertions(+), 53 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 6935947b..f320c076 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -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 diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py index 4dbd1954..30b373ba 100644 --- a/novelwriter/core/indexdata.py +++ b/novelwriter/core/indexdata.py @@ -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 diff --git a/tests/test_core/test_core_indexdata.py b/tests/test_core/test_core_indexdata.py index 85adc7eb..1563e4fe 100644 --- a/tests/test_core/test_core_indexdata.py +++ b/tests/test_core/test_core_indexdata.py @@ -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"" @@ -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) == "" assert repr(head) == "" 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) diff --git a/tests/test_core/test_core_novelmodel.py b/tests/test_core/test_core_novelmodel.py index 8fae210d..b042f5e6 100644 --- a/tests/test_core/test_core_novelmodel.py +++ b/tests/test_core/test_core_novelmodel.py @@ -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) From c1079781730bf8f989e5ed9d9318a8a43ae1d453 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:12:48 +0200 Subject: [PATCH 3/4] Add processing of story structure comments in the build tool --- novelwriter/assets/i18n/project_en_GB.json | 1 + novelwriter/formats/tokenizer.py | 12 +++++++----- sample/content/636b6aa9b697b.nwd | 6 ++++-- sample/nwProject.nwx | 6 +++--- tests/test_formats/test_fmt_tokenizer.py | 8 ++++---- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/novelwriter/assets/i18n/project_en_GB.json b/novelwriter/assets/i18n/project_en_GB.json index 19cdda1e..b5288d04 100644 --- a/novelwriter/assets/i18n/project_en_GB.json +++ b/novelwriter/assets/i18n/project_en_GB.json @@ -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", diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 7d1aeec0..3f3637a7 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -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(( diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index aa2a13ce..5fd4bb76 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -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. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 6b36208e..f9c04fcf 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -58,7 +58,7 @@ Chapter One - + Making a Scene diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index fed6b270..26dd7173 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -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, ""), ] ) From 7d4ae2328c04d821c344d3965af75d5437c01749 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Apr 2025 21:08:43 +0200 Subject: [PATCH 4/4] Improve test coverage --- novelwriter/core/indexdata.py | 4 ++++ tests/test_core/test_core_indexdata.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py index 9a9ce74c..131361f0 100644 --- a/novelwriter/core/indexdata.py +++ b/novelwriter/core/indexdata.py @@ -269,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 diff --git a/tests/test_core/test_core_indexdata.py b/tests/test_core/test_core_indexdata.py index 1563e4fe..9e1080f8 100644 --- a/tests/test_core/test_core_indexdata.py +++ b/tests/test_core/test_core_indexdata.py @@ -217,6 +217,13 @@ def testCoreIndexData_IndexHeading(): 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 @@ -232,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