Add a display name filed to index, and improve handling of it

This commit is contained in:
Veronica Berglyd Olsen
2024-01-29 17:47:30 +01:00
parent ca7ae6b666
commit b396cff9a7
7 changed files with 98 additions and 69 deletions
+62 -31
View File
@@ -32,7 +32,7 @@ import json
import logging import logging
from time import time from time import time
from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator, Literal
from pathlib import Path from pathlib import Path
from novelwriter import SHARED from novelwriter import SHARED
@@ -420,10 +420,11 @@ class NWIndex:
return return
if tBits[0] == nwKeyWords.TAG_KEY: if tBits[0] == nwKeyWords.TAG_KEY:
tagName = tBits[1] tagKey = tBits[1]
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) displayName = tBits[2] if len(tBits) > 2 else tagKey
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName) self._tagsIndex.add(tagKey, displayName, tHandle, sTitle, itemClass)
tags[tagName.lower()] = True self._itemIndex.setHeadingTag(tHandle, sTitle, tagKey)
tags[tagKey.lower()] = True
else: else:
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0]) self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
@@ -471,33 +472,41 @@ class NWIndex:
return True, tBits, tPos return True, tBits, tPos
def checkThese(self, tBits: list[str], nwItem: NWItem) -> list[bool]: def checkThese(self, tBits: list[str], nwItem: NWItem) -> list[Literal[0, 1, 2, 3]]:
"""Check the tags against the index to see if they are valid """Check the tags against the index to see if they are valid
tags. This is needed for syntax highlighting. tags. This is needed for syntax highlighting. The return values
for each item are:
0: Invalid
1: Valid and a keyword
2: Valid and a value
3: Valid and an optional value
""" """
nBits = len(tBits) nBits = len(tBits)
isGood = [False]*nBits
if nBits == 0: if nBits == 0:
return [] return []
# Check that the key is valid # Check that the key is valid
isGood[0] = tBits[0] in nwKeyWords.VALID_KEYS isGood: list[Literal[0, 1, 2, 3]] = [0]*nBits
if not isGood[0] or nBits == 1: isGood[0] = 1 if tBits[0] in nwKeyWords.VALID_KEYS else 0
if isGood[0] == 0 or nBits == 1:
return isGood return isGood
# For a tag, only the first value is accepted, the rest are ignored # For a tag, the first value is the tag, and the second is
# optional and is the display name
if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1: if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
if tBits[1] in self._tagsIndex: if tBits[1] in self._tagsIndex:
isGood[1] = self._tagsIndex.tagHandle(tBits[1]) == nwItem.itemHandle isGood[1] = 2 if self._tagsIndex.tagHandle(tBits[1]) == nwItem.itemHandle else 0
else: else:
isGood[1] = True isGood[1] = 2
if nBits > 2:
isGood[2] = 3
return isGood return isGood
# If we're still here, we check that the references exist # If we're still here, we check that the references exist
refKey = nwKeyWords.KEY_CLASS[tBits[0]].name refKey = nwKeyWords.KEY_CLASS[tBits[0]].name
for n in range(1, nBits): for n in range(1, nBits):
if tBits[n] in self._tagsIndex: if tBits[n] in self._tagsIndex:
isGood[n] = self._tagsIndex.tagClass(tBits[n]) == refKey isGood[n] = 2 if self._tagsIndex.tagClass(tBits[n]) == refKey else 0
return isGood return isGood
@@ -615,9 +624,18 @@ class NWIndex:
for refType in refTypes: for refType in refTypes:
if refType in tRefs: if refType in tRefs:
tRefs[refType].append(self._tagsIndex.tagName(aTag)) tRefs[refType].append(self._tagsIndex.tagName(aTag))
return tRefs return tRefs
def getReferenceForHeader(self, tHandle: str, nHead: int, keyClass: str) -> list[str]:
"""Get the display names for a tags class for insertion into a
heading by one of the build classes.
"""
if iItem := self._itemIndex[tHandle]:
if hItem := iItem[f"T{nHead:04d}"]:
hRefs = [k for k, v in hItem.references.items() if keyClass in v]
return [self._tagsIndex.tagDisplay(k) for k in hRefs]
return []
def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]: def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
"""Build a dict of files referring back to our file.""" """Build a dict of files referring back to our file."""
if tHandle is None or tHandle not in self._itemIndex: if tHandle is None or tHandle not in self._itemIndex:
@@ -715,17 +733,26 @@ class TagsIndex:
"""Return a dictionary view of all tags.""" """Return a dictionary view of all tags."""
return self._tags.items() return self._tags.items()
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None: def add(self, tagKey: str, displayName: str, tHandle: str, sTitle: str,
itemClass: nwItemClass) -> None:
"""Add a key to the index and set all values.""" """Add a key to the index and set all values."""
self._tags[tagKey.lower()] = { self._tags[tagKey.lower()] = {
"name": tagKey, "handle": tHandle, "heading": sTitle, "class": itemClass.name "name": tagKey,
"display": displayName,
"handle": tHandle,
"heading": sTitle,
"class": itemClass.name,
} }
return return
def tagName(self, tagKey: str) -> str: def tagName(self, tagKey: str) -> str:
"""Get the display name of a given tag.""" """Get the name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("name", "") return self._tags.get(tagKey.lower(), {}).get("name", "")
def tagDisplay(self, tagKey: str) -> str:
"""Get the display name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("display", "")
def tagHandle(self, tagKey: str) -> str | None: def tagHandle(self, tagKey: str) -> str | None:
"""Get the handle of a given tag.""" """Get the handle of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("handle", None) return self._tags.get(tagKey.lower(), {}).get("handle", None)
@@ -760,24 +787,28 @@ class TagsIndex:
if not isinstance(data, dict): if not isinstance(data, dict):
raise ValueError("tagsIndex is not a dict") raise ValueError("tagsIndex is not a dict")
for tagKey, tagData in data.items(): for key, entry in data.items():
if not isinstance(tagKey, str): if not isinstance(key, str):
raise ValueError("tagsIndex keys must be a strings") raise ValueError("tagsIndex keys must be a string")
if "name" not in tagData: if "name" not in entry:
raise KeyError("A tagIndex item is missing a name entry") raise KeyError("A tagIndex item is missing a name entry")
if "handle" not in tagData: if "display" not in entry:
raise KeyError("A tagIndex item is missing a display entry")
if "handle" not in entry:
raise KeyError("A tagIndex item is missing a handle entry") raise KeyError("A tagIndex item is missing a handle entry")
if "heading" not in tagData: if "heading" not in entry:
raise KeyError("A tagIndex item is missing a heading entry") raise KeyError("A tagIndex item is missing a heading entry")
if "class" not in tagData: if "class" not in entry:
raise KeyError("A tagIndex item is missing a class entry") raise KeyError("A tagIndex item is missing a class entry")
if tagData["name"].lower() != tagKey: if not isinstance(entry["name"], str):
raise ValueError("tagsIndex name must match key") raise ValueError("tagsIndex name must be a string")
if not isHandle(tagData["handle"]): if not isinstance(entry["display"], str):
raise ValueError("tagsIndex display must be a string")
if not isHandle(entry["handle"]):
raise ValueError("tagsIndex handle must be a handle") raise ValueError("tagsIndex handle must be a handle")
if not isTitleTag(tagData["heading"]): if not isTitleTag(entry["heading"]):
raise ValueError("tagsIndex heading must be a title tag") raise ValueError("tagsIndex heading must be a title tag")
if not isItemClass(tagData["class"]): if not isItemClass(entry["class"]):
raise ValueError("tagsIndex handle must be an nwItemClass") raise ValueError("tagsIndex handle must be an nwItemClass")
self._tags = data self._tags = data
@@ -1165,7 +1196,7 @@ class IndexHeading:
return self._tag return self._tag
@property @property
def references(self) -> dict: def references(self) -> dict[str, set[str]]:
return self._refs return self._refs
## ##
+9 -9
View File
@@ -868,16 +868,16 @@ class HeadingFormatter:
if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat: if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat:
if self._handle and nHead > 0: if self._handle and nHead > 0:
refs = self._project.index.getReferences(self._handle, f"T{nHead:04d}") index = self._project.index
povData = refs[nwKeyWords.POV_KEY] pList = index.getReferenceForHeader(self._handle, nHead, nwKeyWords.POV_KEY)
focData = refs[nwKeyWords.FOCUS_KEY] fList = index.getReferenceForHeader(self._handle, nHead, nwKeyWords.FOCUS_KEY)
povText = povData[0] if povData else nwUnicode.U_ENDASH pText = pList[0] if pList else nwUnicode.U_ENDASH
focText = focData[0] if focData else nwUnicode.U_ENDASH fText = fList[0] if fList else nwUnicode.U_ENDASH
else: else:
povText = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) pText = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
focText = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) fText = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
hFormat = hFormat.replace(nwHeadFmt.CHAR_POV, povText) hFormat = hFormat.replace(nwHeadFmt.CHAR_POV, pText)
hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, focText) hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, fText)
return hFormat return hFormat
+6 -6
View File
@@ -1842,7 +1842,7 @@ class GuiDocEditor(QPlainTextEdit):
return nwTrinary.NEUTRAL return nwTrinary.NEUTRAL
tag = "" tag = ""
exist = False exist = 0
cPos = cursor.selectionStart() - block.position() cPos = cursor.selectionStart() - block.position()
tExist = SHARED.project.index.checkThese(tBits, self._nwItem) tExist = SHARED.project.index.checkThese(tBits, self._nwItem)
for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)): for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)):
@@ -1854,14 +1854,14 @@ class GuiDocEditor(QPlainTextEdit):
exist = sExist exist = sExist
break break
if not tag or tag.startswith("@"): if exist in (1, 3) or not tag:
# The keyword cannot be looked up, so we ignore that # Ignore keywords, optionals and empty tags
return nwTrinary.NEUTRAL return nwTrinary.NEUTRAL
if follow and exist: if follow and exist == 2:
logger.debug("Attempting to follow tag '%s'", tag) logger.debug("Attempting to follow tag '%s'", tag)
self.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW) self.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
elif create and not exist: elif create and exist == 0:
if SHARED.question(self.tr( if SHARED.question(self.tr(
"Do you want to create a new project note for the tag '{0}'?" "Do you want to create a new project note for the tag '{0}'?"
).format(tag)): ).format(tag)):
@@ -1874,7 +1874,7 @@ class GuiDocEditor(QPlainTextEdit):
"If one doesn't exist, you must create one first." "If one doesn't exist, you must create one first."
).format(trConst(nwLabels.CLASS_NAME[itemClass]))) ).format(trConst(nwLabels.CLASS_NAME[itemClass])))
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE return nwTrinary.POSITIVE if exist == 2 else nwTrinary.NEGATIVE
return nwTrinary.NEUTRAL return nwTrinary.NEUTRAL
+10 -12
View File
@@ -35,10 +35,10 @@ from PyQt5.QtGui import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwComment
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwRegEx, nwUnicode from novelwriter.constants import nwRegEx, nwUnicode
from novelwriter.core.index import processComment from novelwriter.core.index import processComment
from novelwriter.enum import nwComment
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -107,7 +107,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"code": self._makeFormat(SHARED.theme.colCode), "code": self._makeFormat(SHARED.theme.colCode),
"keyword": self._makeFormat(SHARED.theme.colKey), "keyword": self._makeFormat(SHARED.theme.colKey),
"modifier": self._makeFormat(SHARED.theme.colMod), "modifier": self._makeFormat(SHARED.theme.colMod),
"value": self._makeFormat(SHARED.theme.colVal, "underline"), "value": self._makeFormat(SHARED.theme.colVal),
"optional": self._makeFormat(SHARED.theme.colOpt),
"codevalue": self._makeFormat(SHARED.theme.colVal), "codevalue": self._makeFormat(SHARED.theme.colVal),
"codeinval": self._makeFormat(None, "errline"), "codeinval": self._makeFormat(None, "errline"),
} }
@@ -286,15 +287,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
for n, bit in enumerate(bits): for n, bit in enumerate(bits):
xPos = pos[n] xPos = pos[n]
xLen = len(bit) xLen = len(bit)
if isGood[n]: if isGood[n] == 1:
if n == 0: self.setFormat(xPos, xLen, self._hStyles["keyword"])
self.setFormat(xPos, xLen, self._hStyles["keyword"]) elif isGood[n] == 2:
else: self.setFormat(xPos, xLen, self._hStyles["value"])
self.setFormat(xPos, xLen, self._hStyles["value"]) elif isGood[n] == 3:
self.setFormat(xPos, xLen, self._hStyles["optional"])
else: else:
kwFmt = self.format(xPos) self.setFormat(xPos, xLen, self._hStyles["codeinval"])
kwFmt.merge(self._hStyles["codeinval"])
self.setFormat(xPos, xLen, kwFmt)
# We never want to run the spell checker on keyword/values, # We never want to run the spell checker on keyword/values,
# so we force a return here # so we force a return here
@@ -406,8 +406,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if "errline" in styles: if "errline" in styles:
charFormat.setUnderlineColor(SHARED.theme.colError) charFormat.setUnderlineColor(SHARED.theme.colError)
charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
if "underline" in styles:
charFormat.setFontUnderline(True)
if "background" in styles and color is not None: if "background" in styles and color is not None:
charFormat.setBackground(QBrush(color, Qt.SolidPattern)) charFormat.setBackground(QBrush(color, Qt.SolidPattern))
+3 -3
View File
@@ -1,11 +1,11 @@
%%~name: John Smith %%~name: John Smith
%%~path: f7e2d9f330615/14298de4d9524 %%~path: f7e2d9f330615/14298de4d9524
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
%%~hash: fda91c416d874aa41a47fceaedb3d62f040c7e32 %%~hash: 0f40182de0bb7935bb40fc4c7fd0d75d55421299
%%~date: Unknown/2023-11-25 18:16:13 %%~date: Unknown/2024-01-29 12:19:42
# John Smith # John Smith
@tag: John @tag: John, John Smith
% Short: The sidekick % Short: The sidekick
+3 -3
View File
@@ -1,11 +1,11 @@
%%~name: Jane Smith %%~name: Jane Smith
%%~path: f7e2d9f330615/bb2c23b3c42cc %%~path: f7e2d9f330615/bb2c23b3c42cc
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
%%~hash: b7291713899bd0356617a36ae606b08e5fee1b65 %%~hash: 0faba71c86841552090a9e82dd9b7acd5b0bf56f
%%~date: Unknown/2023-11-25 18:16:07 %%~date: Unknown/2024-01-29 12:31:34
# Jane Smith # Jane Smith
@tag: Jane @tag: Jane, Jane Smith
% Short: The heroine % Short: The heroine
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3a3" hexVersion="0x020300a3" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-27 18:20:02"> <novelWriterXML appVersion="2.3a3" hexVersion="0x020300a3" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-29 17:42:31">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1621" autoCount="255" editTime="81261"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1660" autoCount="256" editTime="81969">
<name>Sample Project</name> <name>Sample Project</name>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
@@ -57,7 +57,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="2901" wordCount="513" paraCount="15" cursorPos="392" /> <meta expanded="no" heading="H3" charCount="2901" wordCount="513" paraCount="15" cursorPos="66" />
<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">
@@ -101,11 +101,11 @@
<name status="sf12341" import="ia857f0">Main Characters</name> <name status="sf12341" import="ia857f0">Main Characters</name>
</item> </item>
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="48" /> <meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="15" />
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name> <name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
</item> </item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="47" /> <meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="31" />
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name> <name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
</item> </item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD"> <item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">