Add a display name filed to index, and improve handling of it
This commit is contained in:
+62
-31
@@ -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
|
||||
|
||||
##
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user