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
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 novelwriter import SHARED
@@ -420,10 +420,11 @@ class NWIndex:
return
if tBits[0] == nwKeyWords.TAG_KEY:
tagName = tBits[1]
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName)
tags[tagName.lower()] = True
tagKey = tBits[1]
displayName = tBits[2] if len(tBits) > 2 else tagKey
self._tagsIndex.add(tagKey, displayName, tHandle, sTitle, itemClass)
self._itemIndex.setHeadingTag(tHandle, sTitle, tagKey)
tags[tagKey.lower()] = True
else:
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
@@ -471,33 +472,41 @@ class NWIndex:
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
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)
isGood = [False]*nBits
if nBits == 0:
return []
# Check that the key is valid
isGood[0] = tBits[0] in nwKeyWords.VALID_KEYS
if not isGood[0] or nBits == 1:
isGood: list[Literal[0, 1, 2, 3]] = [0]*nBits
isGood[0] = 1 if tBits[0] in nwKeyWords.VALID_KEYS else 0
if isGood[0] == 0 or nBits == 1:
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[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:
isGood[1] = True
isGood[1] = 2
if nBits > 2:
isGood[2] = 3
return isGood
# If we're still here, we check that the references exist
refKey = nwKeyWords.KEY_CLASS[tBits[0]].name
for n in range(1, nBits):
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
@@ -615,9 +624,18 @@ class NWIndex:
for refType in refTypes:
if refType in tRefs:
tRefs[refType].append(self._tagsIndex.tagName(aTag))
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]]:
"""Build a dict of files referring back to our file."""
if tHandle is None or tHandle not in self._itemIndex:
@@ -715,17 +733,26 @@ class TagsIndex:
"""Return a dictionary view of all tags."""
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."""
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
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", "")
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:
"""Get the handle of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("handle", None)
@@ -760,24 +787,28 @@ class TagsIndex:
if not isinstance(data, dict):
raise ValueError("tagsIndex is not a dict")
for tagKey, tagData in data.items():
if not isinstance(tagKey, str):
raise ValueError("tagsIndex keys must be a strings")
if "name" not in tagData:
for key, entry in data.items():
if not isinstance(key, str):
raise ValueError("tagsIndex keys must be a string")
if "name" not in 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")
if "heading" not in tagData:
if "heading" not in 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")
if tagData["name"].lower() != tagKey:
raise ValueError("tagsIndex name must match key")
if not isHandle(tagData["handle"]):
if not isinstance(entry["name"], str):
raise ValueError("tagsIndex name must be a string")
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")
if not isTitleTag(tagData["heading"]):
if not isTitleTag(entry["heading"]):
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")
self._tags = data
@@ -1165,7 +1196,7 @@ class IndexHeading:
return self._tag
@property
def references(self) -> dict:
def references(self) -> dict[str, set[str]]:
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 self._handle and nHead > 0:
refs = self._project.index.getReferences(self._handle, f"T{nHead:04d}")
povData = refs[nwKeyWords.POV_KEY]
focData = refs[nwKeyWords.FOCUS_KEY]
povText = povData[0] if povData else nwUnicode.U_ENDASH
focText = focData[0] if focData else nwUnicode.U_ENDASH
index = self._project.index
pList = index.getReferenceForHeader(self._handle, nHead, nwKeyWords.POV_KEY)
fList = index.getReferenceForHeader(self._handle, nHead, nwKeyWords.FOCUS_KEY)
pText = pList[0] if pList else nwUnicode.U_ENDASH
fText = fList[0] if fList else nwUnicode.U_ENDASH
else:
povText = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
focText = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
hFormat = hFormat.replace(nwHeadFmt.CHAR_POV, povText)
hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, focText)
pText = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
fText = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
hFormat = hFormat.replace(nwHeadFmt.CHAR_POV, pText)
hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, fText)
return hFormat
+6 -6
View File
@@ -1842,7 +1842,7 @@ class GuiDocEditor(QPlainTextEdit):
return nwTrinary.NEUTRAL
tag = ""
exist = False
exist = 0
cPos = cursor.selectionStart() - block.position()
tExist = SHARED.project.index.checkThese(tBits, self._nwItem)
for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)):
@@ -1854,14 +1854,14 @@ class GuiDocEditor(QPlainTextEdit):
exist = sExist
break
if not tag or tag.startswith("@"):
# The keyword cannot be looked up, so we ignore that
if exist in (1, 3) or not tag:
# Ignore keywords, optionals and empty tags
return nwTrinary.NEUTRAL
if follow and exist:
if follow and exist == 2:
logger.debug("Attempting to follow tag '%s'", tag)
self.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
elif create and not exist:
elif create and exist == 0:
if SHARED.question(self.tr(
"Do you want to create a new project note for the tag '{0}'?"
).format(tag)):
@@ -1874,7 +1874,7 @@ class GuiDocEditor(QPlainTextEdit):
"If one doesn't exist, you must create one first."
).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
+10 -12
View File
@@ -35,10 +35,10 @@ from PyQt5.QtGui import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwComment
from novelwriter.common import checkInt
from novelwriter.constants import nwRegEx, nwUnicode
from novelwriter.core.index import processComment
from novelwriter.enum import nwComment
logger = logging.getLogger(__name__)
@@ -107,7 +107,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"code": self._makeFormat(SHARED.theme.colCode),
"keyword": self._makeFormat(SHARED.theme.colKey),
"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),
"codeinval": self._makeFormat(None, "errline"),
}
@@ -286,15 +287,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
for n, bit in enumerate(bits):
xPos = pos[n]
xLen = len(bit)
if isGood[n]:
if n == 0:
self.setFormat(xPos, xLen, self._hStyles["keyword"])
else:
self.setFormat(xPos, xLen, self._hStyles["value"])
if isGood[n] == 1:
self.setFormat(xPos, xLen, self._hStyles["keyword"])
elif isGood[n] == 2:
self.setFormat(xPos, xLen, self._hStyles["value"])
elif isGood[n] == 3:
self.setFormat(xPos, xLen, self._hStyles["optional"])
else:
kwFmt = self.format(xPos)
kwFmt.merge(self._hStyles["codeinval"])
self.setFormat(xPos, xLen, kwFmt)
self.setFormat(xPos, xLen, self._hStyles["codeinval"])
# We never want to run the spell checker on keyword/values,
# so we force a return here
@@ -406,8 +406,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if "errline" in styles:
charFormat.setUnderlineColor(SHARED.theme.colError)
charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
if "underline" in styles:
charFormat.setFontUnderline(True)
if "background" in styles and color is not None:
charFormat.setBackground(QBrush(color, Qt.SolidPattern))
+3 -3
View File
@@ -1,11 +1,11 @@
%%~name: John Smith
%%~path: f7e2d9f330615/14298de4d9524
%%~kind: CHARACTER/NOTE
%%~hash: fda91c416d874aa41a47fceaedb3d62f040c7e32
%%~date: Unknown/2023-11-25 18:16:13
%%~hash: 0f40182de0bb7935bb40fc4c7fd0d75d55421299
%%~date: Unknown/2024-01-29 12:19:42
# John Smith
@tag: John
@tag: John, John Smith
% Short: The sidekick
+3 -3
View File
@@ -1,11 +1,11 @@
%%~name: Jane Smith
%%~path: f7e2d9f330615/bb2c23b3c42cc
%%~kind: CHARACTER/NOTE
%%~hash: b7291713899bd0356617a36ae606b08e5fee1b65
%%~date: Unknown/2023-11-25 18:16:07
%%~hash: 0faba71c86841552090a9e82dd9b7acd5b0bf56f
%%~date: Unknown/2024-01-29 12:31:34
# Jane Smith
@tag: Jane
@tag: Jane, Jane Smith
% Short: The heroine
+5 -5
View File
@@ -1,6 +1,6 @@
<?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">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1621" autoCount="255" editTime="81261">
<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="1660" autoCount="256" editTime="81969">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -57,7 +57,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="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>
</item>
<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>
</item>
<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>
</item>
<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>
</item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">