From b396cff9a7d589d7314ff39812af29cb7feb2d61 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:47:30 +0100 Subject: [PATCH] Add a display name filed to index, and improve handling of it --- novelwriter/core/index.py | 93 +++++++++++++++++++++----------- novelwriter/core/tokenizer.py | 18 +++---- novelwriter/gui/doceditor.py | 12 ++--- novelwriter/gui/dochighlight.py | 22 ++++---- sample/content/14298de4d9524.nwd | 6 +-- sample/content/bb2c23b3c42cc.nwd | 6 +-- sample/nwProject.nwx | 10 ++-- 7 files changed, 98 insertions(+), 69 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 4c58acdd..6fa0a024 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -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 ## diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 267fb0e3..77d4a23d 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -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 diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 6baddf67..6e353687 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -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 diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 5bdf1979..8ce2f462 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -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)) diff --git a/sample/content/14298de4d9524.nwd b/sample/content/14298de4d9524.nwd index 6e4c795d..a0f7992b 100644 --- a/sample/content/14298de4d9524.nwd +++ b/sample/content/14298de4d9524.nwd @@ -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 diff --git a/sample/content/bb2c23b3c42cc.nwd b/sample/content/bb2c23b3c42cc.nwd index f2048c84..b4abf57b 100644 --- a/sample/content/bb2c23b3c42cc.nwd +++ b/sample/content/bb2c23b3c42cc.nwd @@ -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 diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 3a0474f8..54c41384 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -57,7 +57,7 @@ Chapter One - + Making a Scene @@ -101,11 +101,11 @@ Main Characters - + John Smith - + Jane Smith