From 3b7927f0ff306d6192498aea54ddd76bcf805a76 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 25 May 2020 21:14:50 +0200
Subject: [PATCH 1/7] Back-references not consideres all tags in a document,
not just the first one
---
nw/core/index.py | 17 ++++++++---------
nw/gui/elements/doceditor.py | 5 ++++-
nw/gui/elements/docviewer.py | 27 +++++++++++++++++++++++++++
nw/gui/elements/viewdetails.py | 18 ++++++++++++++----
nw/guimain.py | 18 ++++++++++--------
sample/data_a/e7339df26ded_main.nwd | 2 +-
sample/data_f/1471bef9f2ae_main.nwd | 6 ++++++
sample/nwProject.nwx | 22 +++++++++++-----------
8 files changed, 81 insertions(+), 34 deletions(-)
diff --git a/nw/core/index.py b/nw/core/index.py
index e585a03a..79e6ddba 100644
--- a/nw/core/index.py
+++ b/nw/core/index.py
@@ -260,9 +260,9 @@ class NWIndex():
logger.debug("Indexing item with handle %s" % tHandle)
# Check file type, and reset its old index
- # Also add a dummy entry for T0 in case the file has no title
+ # Also add a dummy entry T000000 in case the file has no title
self.refIndex[tHandle] = {}
- self.refIndex[tHandle]["T0"] = {
+ self.refIndex[tHandle]["T000000"] = {
"tags" : [],
"updated" : time(),
}
@@ -606,18 +606,17 @@ class NWIndex():
if tHandle is None:
return theRefs
- theTag = None
+ theTags = []
for tTag in self.tagIndex:
if tHandle == self.tagIndex[tTag][1]:
- theTag = tTag
- break
+ theTags.append(tTag)
- if theTag is not None:
+ if theTags:
for tHandle in self.refIndex:
for sTitle in self.refIndex[tHandle]:
- for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]:
- if tTag == theTag:
- theRefs[tHandle] = nLine
+ for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]:
+ if tTag in theTags and tHandle not in theRefs:
+ theRefs[tHandle] = sTitle
return theRefs
diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py
index 906d4c65..d220a8bd 100644
--- a/nw/gui/elements/doceditor.py
+++ b/nw/gui/elements/doceditor.py
@@ -395,6 +395,8 @@ class GuiDocEditor(QTextEdit):
def setCursorPosition(self, thePosition):
"""Move the cursor to a given position in the document.
"""
+ if not isinstance(thePosition, int):
+ return False
if thePosition >= 0:
theCursor = self.textCursor()
theCursor.setPosition(thePosition)
@@ -410,12 +412,13 @@ class GuiDocEditor(QTextEdit):
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
- if theLine is None:
+ if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
+ logger.verbose("Cursor moved to line %d" % theLine)
return True
##
diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py
index 7fdf22e4..6d90c2f5 100644
--- a/nw/gui/elements/docviewer.py
+++ b/nw/gui/elements/docviewer.py
@@ -206,6 +206,33 @@ class GuiDocViewer(QTextBrowser):
self.docTitle.setTitleFromHandle(self.theHandle)
return
+ ##
+ # Setters
+ ##
+
+ def setCursorPosition(self, thePosition):
+ """Move the cursor to a given position in the document.
+ """
+ if not isinstance(thePosition, int):
+ return False
+ if thePosition >= 0:
+ theCursor = self.textCursor()
+ theCursor.setPosition(thePosition)
+ self.setTextCursor(theCursor)
+ return True
+
+ def setCursorLine(self, theLine):
+ """Move the cursor to a given line in the document.
+ """
+ if not isinstance(theLine, int):
+ return False
+ if theLine >= 0:
+ theBlock = self.qDocument.findBlockByLineNumber(theLine)
+ if theBlock:
+ self.setCursorPosition(theBlock.position())
+ logger.verbose("Cursor moved to line %d" % theLine)
+ return True
+
##
# Events
##
diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py
index 589ffee5..d5ab7ee8 100644
--- a/nw/gui/elements/viewdetails.py
+++ b/nw/gui/elements/viewdetails.py
@@ -107,7 +107,11 @@ class GuiDocViewDetails(QWidget):
for tHandle in theRefs:
tItem = self.theProject.projTree[tHandle]
if tItem is not None:
- theList.append("%s" % (tHandle,tItem.itemName))
+ theList.append("%s" % (
+ tHandle, theRefs[tHandle], tItem.itemName
+ ))
+
+ # print(theList)
self.refList.setText(", ".join(theList))
self.refList.adjustSize()
@@ -122,9 +126,15 @@ class GuiDocViewDetails(QWidget):
"""Capture the link-click and forward it to the document viewer
class for handling.
"""
- if len(theLink) == 18:
- tHandle = theLink[-13:]
- self.theParent.viewDocument(tHandle)
+ logger.verbose("Clicked link: '%s'" % theLink)
+ if len(theLink) == 26:
+ tHandle = theLink[5:18]
+ tLine = theLink[19:26]
+ if tLine[1:].isdigit():
+ nLine = int(tLine[1:])
+ else:
+ nLine = 1
+ self.theParent.viewDocument(tHandle, nLine)
return
def _doShowHide(self, chState):
diff --git a/nw/guimain.py b/nw/guimain.py
index 9c4d5d3a..8aeb7143 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -485,7 +485,7 @@ class GuiMain(QMainWindow):
self.docEditor.saveText()
return True
- def viewDocument(self, tHandle=None):
+ def viewDocument(self, tHandle=None, nLine=0):
"""Load a document for viewing in the view panel.
"""
if tHandle is None:
@@ -503,13 +503,15 @@ class GuiMain(QMainWindow):
# Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitView)
- if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible():
- bPos = self.splitMain.sizes()
- self.viewPane.setVisible(True)
- vPos = [0,0]
- vPos[0] = int(bPos[1]/2)
- vPos[1] = bPos[1]-vPos[0]
- self.splitView.setSizes(vPos)
+ if self.docViewer.loadText(tHandle):
+ if not self.viewPane.isVisible():
+ bPos = self.splitMain.sizes()
+ self.viewPane.setVisible(True)
+ vPos = [0,0]
+ vPos[0] = int(bPos[1]/2)
+ vPos[1] = bPos[1]-vPos[0]
+ self.splitView.setSizes(vPos)
+ self.docViewer.setCursorLine(nLine)
return True
diff --git a/sample/data_a/e7339df26ded_main.nwd b/sample/data_a/e7339df26ded_main.nwd
index 05ed60d5..8b92a69c 100644
--- a/sample/data_a/e7339df26ded_main.nwd
+++ b/sample/data_a/e7339df26ded_main.nwd
@@ -2,6 +2,6 @@
### We Found John!
@pov: John
-@location: Mars
+@location: Mars, OuterSpace
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/data_f/1471bef9f2ae_main.nwd b/sample/data_f/1471bef9f2ae_main.nwd
index 9b2d9368..3bd03e9d 100644
--- a/sample/data_f/1471bef9f2ae_main.nwd
+++ b/sample/data_f/1471bef9f2ae_main.nwd
@@ -5,4 +5,10 @@
Space … it’s an awful lot of nothing, with bits in it here and there. Some of which, people like to call home.
+## Outer Space
+@tag: OuterSpace
+
+Now even further into space!
+
+You can have more than one tag in a file, as long as there is only one tag per heading.
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index ad832035..c2d74892 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
@@ -10,9 +10,9 @@
True
True
- 636b6aa9b697b
- ba8a28a246524
- 914
+ ae7339df26ded
+ ae7339df26ded
+ 941
B
E
@@ -73,7 +73,7 @@
False
True
PAGE
- 208
+ 210
40
2
213
@@ -174,7 +174,7 @@
139
28
1
- 343
+ 237
-
We Found John!
@@ -187,7 +187,7 @@
189
37
1
- 224
+ 236
-
Characters
@@ -257,10 +257,10 @@
False
True
NOTE
- 115
- 24
- 1
- 133
+ 241
+ 51
+ 3
+ 286
-
Mars
From b3e4af1394a671c2054a785b9d8a72489414147b Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 25 May 2020 23:30:55 +0200
Subject: [PATCH 2/7] Added header anchor tags option to the html generator,
and line numbers to the tokens list
---
nw/core/tohtml.py | 37 ++++++++++++---------
nw/core/tokenizer.py | 77 +++++++++++++++++++++++++-------------------
2 files changed, 65 insertions(+), 49 deletions(-)
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 53503ba5..cb2a4baf 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -143,7 +143,7 @@ class ToHtml(Tokenizer):
parStyle = None
tmpResult = []
hasHardBreak = False
- for tType, tText, tFormat, tStyle in self.theTokens:
+ for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Styles
aStyle = []
@@ -174,6 +174,11 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
+ if self.linkHeaders:
+ aNm = "" % (self.theHandle, tLine)
+ else:
+ aNm = ""
+
# Process TextType
if tType == self.T_EMPTY:
if parStyle is None:
@@ -191,23 +196,23 @@ class ToHtml(Tokenizer):
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("%s
\n" % (hStyle, tHead))
+ tmpResult.append("%s%s
\n" % (hStyle, aNm, tHead))
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s>\n" % (h1, hStyle, tHead, h1))
+ tmpResult.append("<%s%s>%s%s%s>\n" % (h1, hStyle, aNm, tHead, h1))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s>\n" % (h2, hStyle, tHead, h2))
+ tmpResult.append("<%s%s>%s%s%s>\n" % (h2, hStyle, aNm, tHead, h2))
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s>\n" % (h3, hStyle, tHead, h3))
+ tmpResult.append("<%s%s>%s%s%s>\n" % (h3, hStyle, aNm, tHead, h3))
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s>\n" % (h4, hStyle, tHead, h4))
+ tmpResult.append("<%s%s>%s%s%s>\n" % (h4, hStyle, aNm, tHead, h4))
elif tType == self.T_SEP:
tmpResult.append("%s
\n" % tText)
@@ -296,17 +301,17 @@ class ToHtml(Tokenizer):
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "%s: " % nwLabels.KEY_NAME[theBits[0]]
- if self.genMode == self.M_PREVIEW:
- for tTag in theBits[1:]:
- refTags.append("%s" % (
- theBits[0][1:], tTag, tTag
- ))
- retText += ", ".join(refTags)
+ if theBits[0] == nwKeyWords.TAG_KEY:
+ retText += "%s" % (
+ theBits[1], theBits[1]
+ )
else:
- if theBits[0] == nwKeyWords.TAG_KEY:
- retText += "%s" % (
- theBits[1], theBits[1]
- )
+ if self.genMode == self.M_PREVIEW:
+ for tTag in theBits[1:]:
+ refTags.append("%s" % (
+ theBits[0][1:], tTag, tTag
+ ))
+ retText += ", ".join(refTags)
else:
for tTag in theBits[1:]:
refTags.append("%s" % (
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index 2fa784f5..fba25fa3 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -102,6 +102,8 @@ class Tokenizer():
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
+ self.linkHeaders = False # Add an anchor before headers
+
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
@@ -174,6 +176,10 @@ class Tokenizer():
self.hideSection = hideSection
return
+ def setLinkHeaders(self, linkHeaders):
+ self.linkHeaders = linkHeaders
+ return
+
def setBodyText(self, doBodyText):
self.doBodyText = doBodyText
return
@@ -307,12 +313,14 @@ class Tokenizer():
self.theTokens = []
self.theMarkdown = ""
tmpMarkdown = []
+ nLine = 0
for aLine in self.theText.splitlines():
+ nLine += 1
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
- self.T_EMPTY, "", None, self.A_NONE
+ self.T_EMPTY, nLine, "", None, self.A_NONE
))
tmpMarkdown.append("\n")
@@ -320,45 +328,45 @@ class Tokenizer():
cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"):
self.theTokens.append((
- self.T_SYNOPSIS, cLine[9:].strip(), None, self.A_NONE
+ self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
))
if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
- self.T_COMMENT, aLine[1:].strip(), None, self.A_NONE
+ self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
))
if self.doComments:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
- self.T_KEYWORD, aLine[1:].strip(), None, self.A_NONE
+ self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
))
if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
- self.T_HEAD1, aLine[2:].strip(), None, self.A_NONE
+ self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
- self.T_HEAD2, aLine[3:].strip(), None, self.A_NONE
+ self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
- self.T_HEAD3, aLine[4:].strip(), None, self.A_NONE
+ self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
- self.T_HEAD4, aLine[5:].strip(), None, self.A_NONE
+ self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
@@ -383,13 +391,13 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
- self.T_TEXT, aLine, fmtPos, self.A_NONE
+ self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
- self.T_EMPTY, "", None, self.A_NONE
+ self.T_EMPTY, nLine, "", None, self.A_NONE
))
tmpMarkdown.append("\n")
@@ -414,7 +422,8 @@ class Tokenizer():
tToken = self.theTokens[n]
tType = tToken[0]
- tText = tToken[1]
+ tLine = tToken[1]
+ tText = tToken[2]
# In case we see text before a scene, we reset the flag
if tType == self.T_TEXT:
@@ -426,7 +435,7 @@ class Tokenizer():
tText = self._formatHeading(self.fmtTitle, tText)
self.theTokens[n] = (
- tType, tText, None, self.A_NONE
+ tType, tLine, tText, None, self.A_NONE
)
elif tType == self.T_HEAD2:
@@ -442,7 +451,7 @@ class Tokenizer():
# Format the chapter header
self.theTokens[n] = (
- tType, tText, None, self.A_PBB
+ tType, tLine, tText, None, self.A_PBB
)
# Set scene variables
@@ -459,29 +468,29 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtScene, tText)
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
- self.T_EMPTY, "", None, self.A_NONE
+ self.T_EMPTY, tLine, "", None, self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
- self.T_EMPTY, "", None, self.A_NONE
+ self.T_EMPTY, tLine, "", None, self.A_NONE
)
else:
self.theTokens[n] = (
- self.T_SKIP, "", None, self.A_NONE
+ self.T_SKIP, tLine, "", None, self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
- self.T_EMPTY, "", None, self.A_NONE
+ self.T_EMPTY, tLine, "", None, self.A_NONE
)
else:
self.theTokens[n] = (
- self.T_SEP, tTemp, None, self.A_CENTRE
+ self.T_SEP, tLine, tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tTemp, None, self.A_NONE
+ tType, tLine, tTemp, None, self.A_NONE
)
# Definitely no longer the first scene
@@ -494,19 +503,19 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtSection, tText)
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
- self.T_EMPTY, "", None, self.A_NONE
+ self.T_EMPTY, tLine, "", None, self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
- self.T_SKIP, "", None, self.A_NONE
+ self.T_SKIP, tLine, "", None, self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
- self.T_SEP, tTemp, None, self.A_CENTRE
+ self.T_SEP, tLine, tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tTemp, None, self.A_NONE
+ tType, tLine, tTemp, None, self.A_NONE
)
# For title page and partitions, we need to centre all text.
@@ -516,20 +525,21 @@ class Tokenizer():
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
- tText = tToken[1]
- tFormat = tToken[2]
+ tLine = tToken[1]
+ tText = tToken[2]
+ tFormat = tToken[3]
if tType == self.T_HEAD1:
if self.isTitle:
self.theTokens[n] = (
- self.T_TITLE, tText, tFormat, self.A_PBB_NO | self.A_CENTRE
+ self.T_TITLE, tLine, tText, tFormat, self.A_PBB_NO | self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tText, tFormat, self.A_PBB | self.A_CENTRE
+ tType, tLine, tText, tFormat, self.A_PBB | self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tText, tFormat, self.A_CENTRE
+ tType, tLine, tText, tFormat, self.A_CENTRE
)
# Add a page break after the last entry
@@ -537,7 +547,7 @@ class Tokenizer():
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
- tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA
+ tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
@@ -545,15 +555,16 @@ class Tokenizer():
if self.isPage:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
- tText = tToken[1]
- tFormat = tToken[2]
+ tLine = tToken[1]
+ tText = tToken[2]
+ tFormat = tToken[3]
if n == 0:
self.theTokens[n] = (
- tType, tText, tFormat, self.A_LEFT | self.A_PBB
+ tType, tLine, tText, tFormat, self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
- tType, tText, tFormat, self.A_LEFT
+ tType, tLine, tText, tFormat, self.A_LEFT
)
return
From 4340b6b8b5725323c9366712f27ec54bdf9ffd56 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 25 May 2020 23:31:41 +0200
Subject: [PATCH 3/7] It should now be possible to navigate to positions within
the documents when clicking links in various places
---
nw/core/index.py | 21 +++++++-------
nw/gui/elements/docviewer.py | 52 +++++++++++++++-------------------
nw/gui/elements/viewdetails.py | 15 +++-------
nw/guimain.py | 4 +--
4 files changed, 40 insertions(+), 52 deletions(-)
diff --git a/nw/core/index.py b/nw/core/index.py
index 79e6ddba..456b415c 100644
--- a/nw/core/index.py
+++ b/nw/core/index.py
@@ -199,7 +199,7 @@ class NWIndex():
try:
for tTag in self.tagIndex:
- if len(self.tagIndex[tTag]) != 3:
+ if len(self.tagIndex[tTag]) != 4:
self.indexBroken = True
for tHandle in self.refIndex:
@@ -228,7 +228,7 @@ class NWIndex():
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
- "The index loaded from project cache contains errors. Rebuilding index.",
+ "The project index is outdated or broken. Rebuilding index.",
nwAlert.WARN
)
@@ -301,7 +301,7 @@ class NWIndex():
elif aLine.startswith(r"@"):
self._indexNoteRef(tHandle, aLine, nLine, nTitle)
- self._indexTag(tHandle, aLine, nLine, itemClass)
+ self._indexTag(tHandle, aLine, nLine, nTitle, itemClass)
elif aLine.startswith(r"%"):
if nTitle > 0:
@@ -436,7 +436,7 @@ class NWIndex():
return True
- def _indexTag(self, tHandle, aLine, nLine, itemClass):
+ def _indexTag(self, tHandle, aLine, nLine, nTitle, itemClass):
"""Validate and save the information from a tag.
"""
isValid, theBits, thePos = self.scanThis(aLine)
@@ -444,7 +444,8 @@ class NWIndex():
return False
if theBits[0] == nwKeyWords.TAG_KEY:
- self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name]
+ sTitle = "T%06d" % nTitle
+ self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
return True
@@ -606,10 +607,10 @@ class NWIndex():
if tHandle is None:
return theRefs
- theTags = []
+ theTags = set()
for tTag in self.tagIndex:
if tHandle == self.tagIndex[tTag][1]:
- theTags.append(tTag)
+ theTags.add(tTag)
if theTags:
for tHandle in self.refIndex:
@@ -625,8 +626,8 @@ class NWIndex():
"""
if theTag in self.tagIndex:
theRef = self.tagIndex[theTag]
- if len(theRef) == 3:
- return theRef[1], theRef[0]
- return None, 0
+ if len(theRef) == 4:
+ return theRef[1], theRef[0], theRef[3]
+ return None, 0, "T000000"
# END Class NWIndex
diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py
index 6d90c2f5..df1355be 100644
--- a/nw/gui/elements/docviewer.py
+++ b/nw/gui/elements/docviewer.py
@@ -28,7 +28,7 @@
import logging
import nw
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QTextBrowser
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
@@ -133,6 +133,7 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments)
+ aDoc.setLinkHeaders(True)
aDoc.setText(tHandle)
aDoc.doAutoReplace()
aDoc.tokenizeText()
@@ -162,19 +163,17 @@ class GuiDocViewer(QTextBrowser):
index being up to date.
"""
logger.debug("Loading document from tag '%s'" % theTag)
-
- if theTag in self.theParent.theIndex.tagIndex.keys():
- theTarget = self.theParent.theIndex.tagIndex[theTag]
+ tHandle, onLine, sTitle = self.theParent.theIndex.getTagSource(theTag)
+ if tHandle is None:
+ self.theParent.makeAlert((
+ "Could not find the reference for tag '%s'. It either doesn't "
+ "exist, or the index is out of date. The index can be updated "
+ "from the Tools menu, or by pressing F9."
+ ) % theTag, nwAlert.ERROR)
+ return
else:
- logger.debug("The tag was not found in the index")
- return False
-
- if len(theTarget) != 3:
- # Just to make sure the index is not messed up
- return False
-
- self.loadText(theTarget[1])
-
+ self.loadText(tHandle)
+ self.navigateTo("#head_%s:%s" % (tHandle, sTitle))
return True
def docAction(self, theAction):
@@ -198,6 +197,15 @@ class GuiDocViewer(QTextBrowser):
return False
return True
+ def navigateTo(self, navLink):
+ """Go to a specific #link in the document.
+ """
+ if not isinstance(navLink, str):
+ return False
+ if navLink.startswith("#"):
+ self.setSource(QUrl(navLink))
+ return True
+
def updateDocTitle(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
@@ -271,25 +279,11 @@ class GuiDocViewer(QTextBrowser):
"""Slot for a link in the document being clicked.
"""
theLink = theURL.url()
- tHandle = None
- onLine = 0
- theTag = ""
+ logger.verbose("Clicked link: '%s'" % theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
- theTag = theBits[1]
- tHandle, onLine = self.theParent.theIndex.getTagSource(theBits[1])
-
- if tHandle is None:
- self.theParent.makeAlert((
- "Could not find the reference for tag '%s'. It either doesn't exist, or the index "
- "is out of date. The index can be updated from the Tools menu.") % theTag,
- nwAlert.ERROR
- )
- return
- else:
- self.loadText(tHandle)
-
+ self.loadFromTag(theBits[1])
return
def _makeStyleSheet(self):
diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py
index d5ab7ee8..d4b2621c 100644
--- a/nw/gui/elements/viewdetails.py
+++ b/nw/gui/elements/viewdetails.py
@@ -107,12 +107,10 @@ class GuiDocViewDetails(QWidget):
for tHandle in theRefs:
tItem = self.theProject.projTree[tHandle]
if tItem is not None:
- theList.append("%s" % (
+ theList.append("%s" % (
tHandle, theRefs[tHandle], tItem.itemName
))
- # print(theList)
-
self.refList.setText(", ".join(theList))
self.refList.adjustSize()
@@ -127,14 +125,9 @@ class GuiDocViewDetails(QWidget):
class for handling.
"""
logger.verbose("Clicked link: '%s'" % theLink)
- if len(theLink) == 26:
- tHandle = theLink[5:18]
- tLine = theLink[19:26]
- if tLine[1:].isdigit():
- nLine = int(tLine[1:])
- else:
- nLine = 1
- self.theParent.viewDocument(tHandle, nLine)
+ if len(theLink) == 27:
+ tHandle = theLink[6:19]
+ self.theParent.viewDocument(tHandle, theLink)
return
def _doShowHide(self, chState):
diff --git a/nw/guimain.py b/nw/guimain.py
index 8aeb7143..f0d0d3cc 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -485,7 +485,7 @@ class GuiMain(QMainWindow):
self.docEditor.saveText()
return True
- def viewDocument(self, tHandle=None, nLine=0):
+ def viewDocument(self, tHandle=None, navLink=None):
"""Load a document for viewing in the view panel.
"""
if tHandle is None:
@@ -511,7 +511,7 @@ class GuiMain(QMainWindow):
vPos[0] = int(bPos[1]/2)
vPos[1] = bPos[1]-vPos[0]
self.splitView.setSizes(vPos)
- self.docViewer.setCursorLine(nLine)
+ self.docViewer.navigateTo(navLink)
return True
From 494997cc202f8469e3c6bacbb6329f3c86b7c872 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Mon, 25 May 2020 23:31:59 +0200
Subject: [PATCH 4/7] Fixed tests
---
sample/nwProject.nwx | 4 ++--
tests/test_project.py | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index c2d74892..2dce2260 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
@@ -11,7 +11,7 @@
True
True
ae7339df26ded
- ae7339df26ded
+ bb2c23b3c42cc
941
B
diff --git a/tests/test_project.py b/tests/test_project.py
index 369c866d..44821856 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -155,7 +155,7 @@ def testIndexCheckThese(nwTempProj):
"# Hello World!\n"
"@pov: Jane"
))
- assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}"
+ assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
@@ -195,7 +195,7 @@ def testIndexMeta(nwTempProj):
"\n"
"Well, not really.\n"
))
- assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}"
+ assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
# The novel structure should contain the pointer to the novel file header
@@ -214,6 +214,6 @@ def testIndexMeta(nwTempProj):
# The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle)
- assert str(theRefs) == "{'41cfc0d1f2d12': 3}"
+ assert str(theRefs) == "{'41cfc0d1f2d12': 'T000001'}"
assert theProject.closeProject()
From abe549c65ad5213ecee0b766f5e98febc1a88c4e Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 28 May 2020 19:40:11 +0200
Subject: [PATCH 5/7] Updated changelog
---
CHANGELOG.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dfd9e8cd..732de25e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
## Version 0.7 [2020-xx-xx]
+**User Interface**
+
+* The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234.
+* Clicking a tag should also tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234.
+
**Project Structure**
* The project folder structure has been simplified and cleaned up. We also now pin the main entry values in the main XML file. the XML file is now given version 1.1, and locking it to only be opened by version 0.7 or later. The project is converted on first open, if the user approves. PR #253.
From 65f74e6fdcf3bd3dc4e823ce7f385993f6939eaa Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 28 May 2020 20:01:44 +0200
Subject: [PATCH 6/7] Fixed typo in changelog
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed683147..4ea1d757 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
**User Interface**
* The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234.
-* Clicking a tag should also tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234.
+* Clicking a tag now tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234.
**Project Structure**
From 2bbde2c4451286c135c0caaa9940e6336764e02a Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Thu, 28 May 2020 20:19:55 +0200
Subject: [PATCH 7/7] Improved the tokenizer class a bit
---
nw/core/tokenizer.py | 190 +++++++++++++++++++++++++++++++------------
nw/gui/build.py | 3 -
sample/nwProject.nwx | 21 ++---
3 files changed, 141 insertions(+), 73 deletions(-)
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index fb632140..1dc573ca 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -320,7 +320,9 @@ class Tokenizer():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
- self.T_EMPTY, nLine, "", None, self.A_NONE
+ self.T_EMPTY, nLine,
+ "", None,
+ self.A_NONE
))
tmpMarkdown.append("\n")
@@ -328,45 +330,59 @@ class Tokenizer():
cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"):
self.theTokens.append((
- self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
+ self.T_SYNOPSIS, nLine,
+ cLine[9:].strip(), None,
+ self.A_NONE
))
if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
- self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
+ self.T_COMMENT, nLine,
+ aLine[1:].strip(), None,
+ self.A_NONE
))
if self.doComments:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
- self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
+ self.T_KEYWORD, nLine,
+ aLine[1:].strip(), None,
+ self.A_NONE
))
if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
- self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
+ self.T_HEAD1, nLine,
+ aLine[2:].strip(), None,
+ self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
- self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
+ self.T_HEAD2, nLine,
+ aLine[3:].strip(), None,
+ self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
- self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
+ self.T_HEAD3, nLine,
+ aLine[4:].strip(), None,
+ self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
- self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
+ self.T_HEAD4, nLine,
+ aLine[5:].strip(), None,
+ self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
@@ -391,13 +407,17 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
- self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
+ self.T_TEXT, nLine,
+ aLine, fmtPos,
+ self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
- self.T_EMPTY, nLine, "", None, self.A_NONE
+ self.T_EMPTY, nLine,
+ "", None,
+ self.A_NONE
))
tmpMarkdown.append("\n")
@@ -421,101 +441,146 @@ class Tokenizer():
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
- tType = tToken[0]
- tLine = tToken[1]
- tText = tToken[2]
# In case we see text before a scene, we reset the flag
- if tType == self.T_TEXT:
+ if tToken[0] == self.T_TEXT:
self.firstScene = False
- elif tType == self.T_HEAD1:
+ elif tToken[0] == self.T_HEAD1:
# Main Title
# ==========
- tText = self._formatHeading(self.fmtTitle, tText)
+ tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = (
- tType, tLine, tText, None, self.A_NONE
+ tToken[0],
+ tToken[1],
+ tTemp,
+ None,
+ self.A_NONE
)
- elif tType == self.T_HEAD2:
+ elif tToken[0] == self.T_HEAD2:
# Novel Chapter
# =============
# Numbered or Unnumbered
if self.isUnNum:
- tText = self._formatHeading(self.fmtUnNum, tText)
+ tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
else:
self.numChapter += 1
- tText = self._formatHeading(self.fmtChapter, tText)
+ tTemp = self._formatHeading(self.fmtChapter, tToken[2])
# Format the chapter header
self.theTokens[n] = (
- tType, tLine, tText, None, self.A_PBB
+ tToken[0],
+ tToken[1],
+ tTemp,
+ None,
+ self.A_PBB
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
- elif tType == self.T_HEAD3:
+ elif tToken[0] == self.T_HEAD3:
# Novel Scene
# ===========
self.numChScene += 1
self.numAbsScene += 1
- tTemp = self._formatHeading(self.fmtScene, tText)
+ tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
- self.T_EMPTY, tLine, "", None, self.A_NONE
+ self.T_EMPTY,
+ tToken[1],
+ "",
+ None,
+ self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
- self.T_EMPTY, tLine, "", None, self.A_NONE
+ self.T_EMPTY,
+ tToken[1],
+ "",
+ None,
+ self.A_NONE
)
else:
self.theTokens[n] = (
- self.T_SKIP, tLine, "", None, self.A_NONE
+ self.T_SKIP,
+ tToken[1],
+ "",
+ None,
+ self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
- self.T_EMPTY, tLine, "", None, self.A_NONE
+ self.T_EMPTY,
+ tToken[1],
+ "",
+ None,
+ self.A_NONE
)
else:
self.theTokens[n] = (
- self.T_SEP, tLine, tTemp, None, self.A_CENTRE
+ self.T_SEP,
+ tToken[1],
+ tTemp,
+ None,
+ self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tLine, tTemp, None, self.A_NONE
+ tToken[0],
+ tToken[1],
+ tTemp,
+ None,
+ self.A_NONE
)
# Definitely no longer the first scene
self.firstScene = False
- elif tType == self.T_HEAD4:
+ elif tToken[0] == self.T_HEAD4:
# Novel Section
# =============
- tTemp = self._formatHeading(self.fmtSection, tText)
+ tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
- self.T_EMPTY, tLine, "", None, self.A_NONE
+ self.T_EMPTY,
+ tToken[1],
+ "",
+ None,
+ self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
- self.T_SKIP, tLine, "", None, self.A_NONE
+ self.T_SKIP,
+ tToken[1],
+ "",
+ None,
+ self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
- self.T_SEP, tLine, tTemp, None, self.A_CENTRE
+ self.T_SEP,
+ tToken[1],
+ tTemp,
+ None,
+ self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tLine, tTemp, None, self.A_NONE
+ tToken[0],
+ tToken[1],
+ tTemp,
+ None,
+ self.A_NONE
)
# For title page and partitions, we need to centre all text.
@@ -524,22 +589,30 @@ class Tokenizer():
# We also swap header level 1 with a title type instead.
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
- tType = tToken[0]
- tLine = tToken[1]
- tText = tToken[2]
- tFormat = tToken[3]
- if tType == self.T_HEAD1:
+ if tToken[0] == self.T_HEAD1:
if self.isTitle:
self.theTokens[n] = (
- self.T_TITLE, tLine, tText, tFormat, self.A_PBB_NO | self.A_CENTRE
+ self.T_TITLE,
+ tToken[1],
+ tToken[2],
+ tToken[3],
+ self.A_PBB_NO | self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tLine, tText, tFormat, self.A_PBB | self.A_CENTRE
+ tToken[0],
+ tToken[1],
+ tToken[2],
+ tToken[3],
+ self.A_PBB | self.A_CENTRE
)
else:
self.theTokens[n] = (
- tType, tLine, tText, tFormat, self.A_CENTRE
+ tToken[0],
+ tToken[1],
+ tToken[2],
+ tToken[3],
+ self.A_CENTRE
)
# Add a page break after the last entry
@@ -547,24 +620,32 @@ class Tokenizer():
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
- tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
+ tToken[0],
+ tToken[1],
+ tToken[2],
+ tToken[3],
+ tToken[4] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
# page, unless it's empty.
if self.isPage:
for n, tToken in enumerate(self.theTokens):
- tType = tToken[0]
- tLine = tToken[1]
- tText = tToken[2]
- tFormat = tToken[3]
if n == 0:
self.theTokens[n] = (
- tType, tLine, tText, tFormat, self.A_LEFT | self.A_PBB
+ tToken[0],
+ tToken[1],
+ tToken[2],
+ tToken[3],
+ self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
- tType, tLine, tText, tFormat, self.A_LEFT
+ tToken[0],
+ tToken[1],
+ tToken[2],
+ tToken[3],
+ self.A_LEFT
)
return
@@ -577,10 +658,11 @@ class Tokenizer():
"""Replaces the %keyword% strings.
"""
theTitle = theTitle.replace(r"%title%", theText)
- theTitle = theTitle.replace(r"%ch%", str(self.numChapter))
- theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
- theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
- theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en"))
+ theTitle = theTitle.replace(r"%ch%", str(self.numChapter))
+ theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
+ theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
+ if r"%chw%" in theTitle:
+ theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en"))
return theTitle
# END Class Tokenizer
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 6a7c99cc..b73f1017 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -294,9 +294,6 @@ class GuiBuildNovel(QDialog):
# ==============
self.buttonForm = QGridLayout()
- self.btnHelp = QPushButton("Help")
- self.btnHelp.clicked.connect(self._showHelp)
-
self.btnPrint = QPushButton("Print")
self.btnPrint.clicked.connect(self._printDocument)
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index abe767ea..b6f63962 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
@@ -11,8 +11,8 @@
True
True
636b6aa9b697b
- ba8a28a246524
- 914
+ bc0cbd2a407f3
+ 941
B
E
@@ -122,7 +122,7 @@
1199
216
7
- 825
+ 1066
-
Another Scene
@@ -174,11 +174,7 @@
139
28
1
-<<<<<<< HEAD
- 237
-=======
242
->>>>>>> dev
-
We Found John!
@@ -191,7 +187,7 @@
189
37
1
- 236
+ 224
-
Characters
@@ -261,17 +257,10 @@
False
True
NOTE
-<<<<<<< HEAD
241
51
3
- 286
-=======
- 115
- 24
- 1
135
->>>>>>> dev
-
Mars