diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index cdd78032..aba00ba5 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -227,7 +227,7 @@ class NWIndex: """Scan a piece of text associated with a handle. This will update the indices accordingly. This function takes the handle and text as separate inputs as we want to primarily scan the - files before we save them in which case we already have the + files before we save them, in which case we already have the text. """ theItem = self.theProject.tree[tHandle] @@ -238,9 +238,8 @@ class NWIndex: logger.info("Not indexing non-file item '%s'", tHandle) return False - # Delete tags and create new item entry - for tTag in self._itemIndex.allItemTags(tHandle): - del self._tagsIndex[tTag] + # Keep a record of existing tags, and create a new item entry + itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False) self._itemIndex.add(tHandle, theItem) # Run word counter for the whole text @@ -279,7 +278,7 @@ class NWIndex: nTitle = nLine elif aLine.startswith("@"): - self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass) + self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass, itemTags) elif aLine.startswith("%"): if nTitle > 0: @@ -300,6 +299,12 @@ class NWIndex: if nTitle == 0: self._indexWordCounts(tHandle, theText, nTitle) + # Prune no longer used tags + for tTag, isActive in itemTags.items(): + if not isActive: + logger.verbose("Deleting removed tag '%s'", tTag) + del self._tagsIndex[tTag] + # Update timestamps for index changes nowTime = round(time()) self._timeIndex = nowTime @@ -359,9 +364,11 @@ class NWIndex: self._itemIndex.setHeadingSynopsis(tHandle, sTitle, theText) return - def _indexKeyword(self, tHandle, aLine, nTitle, itemClass): + def _indexKeyword(self, tHandle, aLine, nTitle, itemClass, itemTags): """Validate and save the information about a reference to a tag - in another file. + in another file, or the setting of a tag in the file. A record + of active tags is updated so that no longer used tags can be + pruned later. """ isValid, theBits, _ = self.scanThis(aLine) if not isValid or len(theBits) < 2: @@ -374,8 +381,10 @@ class NWIndex: sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: - self._tagsIndex.add(theBits[1], tHandle, sTitle, itemClass) - self._itemIndex.setHeadingTag(tHandle, sTitle, theBits[1]) + tagName = theBits[1] + self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) + self._itemIndex.setHeadingTag(tHandle, sTitle, tagName) + itemTags[tagName] = True else: self._itemIndex.addHeadingReferences(tHandle, sTitle, theBits[1:], theBits[0]) @@ -884,6 +893,11 @@ class ItemIndex: class IndexItem: + """This object represents the index data of a project item (NWItem). + It holds a record of all the headings in the text, and the meta data + associated with each heading. It also holds a pointer to the project + item. + """ def __init__(self, tHandle, tItem): self._handle = tHandle @@ -1027,6 +1041,10 @@ class IndexItem: class IndexHeading: + """This object represents a section of text in a project item + associated with a single (valid) heading. It holds a separate record + of all references made under each heading. + """ def __init__(self, key, level="H0", title=""): self._key = key @@ -1148,7 +1166,7 @@ class IndexHeading: def packReferences(self): """Pack references into a dictionary for saving to cache. """ - return {key: list(value) for key, value in self._refs.items()} + return {key: ",".join(value) for key, value in self._refs.items()} def unpackData(self, data): """Unpack a heading entry from a dictionary. @@ -1170,9 +1188,9 @@ class IndexHeading: for tagKey, refTypes in data.items(): if not isinstance(tagKey, str): raise ValueError("itemIndex reference key must be a string") - if not isinstance(refTypes, list): - raise ValueError("itemIndex reference types must be a list") - for refType in refTypes: + if not isinstance(refTypes, str): + raise ValueError("itemIndex reference types must be a string") + for refType in refTypes.split(","): if refType in nwKeyWords.VALID_KEYS: self.addReference(tagKey, refType) else: diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 20a66690..a334e8ce 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -4,7 +4,7 @@ ### Making a Scene @pov: Jane -@char: John +@char: John, Jane @location: Earth 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. diff --git a/sample/content/88706ddc78b1b.nwd b/sample/content/88706ddc78b1b.nwd index 0f140538..ceaddd29 100644 --- a/sample/content/88706ddc78b1b.nwd +++ b/sample/content/88706ddc78b1b.nwd @@ -11,6 +11,7 @@ ### Jane Cannot Find John @pov: Jane +@focus: John @location: Space Jane has been looking all over for John. He’s nowhere to be found on Earth, so Jane goes to space. diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index 1eb7a65d..8b53f816 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -4,6 +4,7 @@ ### We Found John! @pov: John +@focus: John @location: Mars Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes. diff --git a/sample/content/b3e74dbc1f584.nwd b/sample/content/b3e74dbc1f584.nwd index c980865e..bb88600b 100644 --- a/sample/content/b3e74dbc1f584.nwd +++ b/sample/content/b3e74dbc1f584.nwd @@ -6,4 +6,4 @@ @tag: Earth @location: Space -Third planet from the sun, fairly dense, and with lots of people on it. \ No newline at end of file +Third planet from the sun, fairly dense, and with lots of people on it. diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index fafdeb68..60c59d86 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -35,7 +35,7 @@ "T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "88243afbe5ed8": { @@ -45,7 +45,7 @@ "T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "f96ec11c6a3da": { @@ -55,7 +55,7 @@ "T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "846352075de7d": { @@ -70,7 +70,7 @@ "T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "eb103bc70c90c": { @@ -79,7 +79,7 @@ "T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "f8c0562e50f1b": { @@ -88,7 +88,7 @@ "T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "47666c91c7ccf": { @@ -97,7 +97,7 @@ "T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} }, "references": { - "T000001": {"Bod": ["@pov"], "Main": ["@plot"], "Europe": ["@location"]} + "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "4c4f28287af27": { @@ -106,7 +106,7 @@ "T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} }, "references": { - "T000001": {"Main": ["@plot"]} + "T000001": {"Main": "@plot"} } }, "2426c6f0ca922": {