Add project note briefs (#1618)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-25 19:01:15 +01:00
committed by GitHub
21 changed files with 251 additions and 67 deletions
@@ -1,6 +1,7 @@
{ {
"Synopsis": "Synopsis", "Synopsis": "Synopsis",
"Comment": "Comment", "Comment": "Comment",
"Brief": "Brief",
"Notes": "Notes", "Notes": "Notes",
"Tag": "Tag", "Tag": "Tag",
"Point of View": "Point of View", "Point of View": "Point of View",
+6 -1
View File
@@ -406,6 +406,7 @@ class ProjectBuilder:
chSynop = self.tr("Summary of the chapter.") chSynop = self.tr("Summary of the chapter.")
scSynop = self.tr("Summary of the scene.") scSynop = self.tr("Summary of the scene.")
bfNote = self.tr("A brief description.")
# Create chapters # Create chapters
if numChapters > 0: if numChapters > 0:
@@ -446,7 +447,11 @@ class ProjectBuilder:
aHandle = project.newFile(noteTitles[newRoot], rHandle) aHandle = project.newFile(noteTitles[newRoot], rHandle)
ntTag = simplified(noteTitles[newRoot]).replace(" ", "") ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
aDoc = project.storage.getDocument(aHandle) aDoc = project.storage.getDocument(aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n") aDoc.writeDocument(
f"# {noteTitles[newRoot]}\n\n"
f"@tag: {ntTag}\n\n"
f"% Brief: {bfNote}\n\n"
)
# Also add the archive and trash folders # Also add the archive and trash folders
project.newRoot(nwItemClass.ARCHIVE) project.newRoot(nwItemClass.ARCHIVE)
+22 -10
View File
@@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
from pathlib import Path from pathlib import Path
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
@@ -338,14 +338,9 @@ class NWIndex:
elif line.startswith("%"): elif line.startswith("%"):
if cTitle != TT_NONE: if cTitle != TT_NONE:
toCheck = line[1:].lstrip() cStyle, cText, _ = processComment(line)
synTag = toCheck[:9].lower() if cStyle in (nwComment.BRIEF, nwComment.SYNOPSIS):
tLen = len(line) self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
cLen = len(toCheck)
cOff = tLen - cLen
if synTag == "synopsis:":
sText = line[cOff+9:].strip()
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
# Count words for remaining text after last heading # Count words for remaining text after last heading
if pTitle != TT_NONE: if pTitle != TT_NONE:
@@ -1269,9 +1264,26 @@ class IndexHeading:
# =============================================================================================== # # =============================================================================================== #
# Simple Word Counter # Text Processing Functions
# =============================================================================================== # # =============================================================================================== #
CLASSIFIERS = {
"brief": nwComment.BRIEF,
"synopsis": nwComment.SYNOPSIS,
}
def processComment(text: str) -> tuple[nwComment, str, int]:
"""Extract comment style and text. Should only be called on text
starting with a %.
"""
check = text[1:].lstrip()
classifier, _, content = check.partition(":")
if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
return nwComment.PLAIN, check, 0
def countWords(text: str) -> tuple[int, int, int]: def countWords(text: str) -> tuple[int, int, int]:
"""Count words in a piece of text, skipping special syntax and """Count words in a piece of text, skipping special syntax and
comments. comments.
+3
View File
@@ -69,6 +69,9 @@ VALID_MAP = {
"GuiManuscriptBuild": { "GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "sumWidth", "winWidth", "winHeight", "fmtWidth", "sumWidth",
}, },
"GuiDocViewerPanel": {
"colWidths",
}
} }
+6 -3
View File
@@ -287,7 +287,10 @@ class ToHtml(Tokenizer):
para.append(stripEscape(tTemp.rstrip())) para.append(stripEscape(tTemp.rstrip()))
elif tType == self.T_SYNOPSIS and self._doSynopsis: elif tType == self.T_SYNOPSIS and self._doSynopsis:
lines.append(self._formatSynopsis(tText)) lines.append(self._formatSynopsis(tText, True))
elif tType == self.T_BRIEF and self._doSynopsis:
lines.append(self._formatSynopsis(tText, False))
elif tType == self.T_COMMENT and self._doComments: elif tType == self.T_COMMENT and self._doComments:
lines.append(self._formatComments(tText)) lines.append(self._formatComments(tText))
@@ -454,9 +457,9 @@ class ToHtml(Tokenizer):
# Internal Functions # Internal Functions
## ##
def _formatSynopsis(self, text: str) -> str: def _formatSynopsis(self, text: str, synopsis: bool) -> str:
"""Apply HTML formatting to synopsis.""" """Apply HTML formatting to synopsis."""
sSynop = self._localLookup("Synopsis") sSynop = self._localLookup("Synopsis") if synopsis else self._localLookup("Brief")
if self._genMode == self.M_PREVIEW: if self._genMode == self.M_PREVIEW:
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n" return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
else: else:
+24 -17
View File
@@ -34,8 +34,9 @@ from pathlib import Path
from functools import partial from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter.core.index import processComment
from novelwriter.enum import nwItemLayout from novelwriter.enum import nwComment, nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import nwHeadFmt, nwRegEx, nwShortcode, nwUnicode from novelwriter.constants import nwHeadFmt, nwRegEx, nwShortcode, nwUnicode
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
@@ -79,17 +80,18 @@ class Tokenizer(ABC):
# Block Type # Block Type
T_EMPTY = 1 # Empty line (new paragraph) T_EMPTY = 1 # Empty line (new paragraph)
T_SYNOPSIS = 2 # Synopsis comment T_SYNOPSIS = 2 # Synopsis comment
T_COMMENT = 3 # Comment line T_BRIEF = 3 # Brief comment
T_KEYWORD = 4 # Command line T_COMMENT = 4 # Comment line
T_TITLE = 5 # Title T_KEYWORD = 5 # Command line
T_UNNUM = 6 # Unnumbered T_TITLE = 6 # Title
T_HEAD1 = 7 # Header 1 T_UNNUM = 7 # Unnumbered
T_HEAD2 = 8 # Header 2 T_HEAD1 = 8 # Header 1
T_HEAD3 = 9 # Header 3 T_HEAD2 = 9 # Header 2
T_HEAD4 = 10 # Header 4 T_HEAD3 = 10 # Header 3
T_TEXT = 11 # Text line T_HEAD4 = 11 # Header 4
T_SEP = 12 # Scene separator T_TEXT = 12 # Text line
T_SKIP = 13 # Paragraph break T_SEP = 13 # Scene separator
T_SKIP = 14 # Paragraph break
# Block Style # Block Style
A_NONE = 0x0000 # No special style A_NONE = 0x0000 # No special style
@@ -461,17 +463,22 @@ class Tokenizer(ABC):
continue continue
if aLine[0] == "%": if aLine[0] == "%":
cLine = aLine[1:].lstrip() cStyle, cText, _ = processComment(aLine)
synTag = cLine[:9].lower() if cStyle == nwComment.SYNOPSIS:
if synTag == "synopsis:":
self._tokens.append(( self._tokens.append((
self.T_SYNOPSIS, nHead, cLine[9:].strip(), None, sAlign self.T_SYNOPSIS, nHead, cText, None, sAlign
))
if self._doSynopsis and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif cStyle == nwComment.BRIEF:
self._tokens.append((
self.T_BRIEF, nHead, cText, None, sAlign
)) ))
if self._doSynopsis and self._keepMarkdown: if self._doSynopsis and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
else: else:
self._tokens.append(( self._tokens.append((
self.T_COMMENT, nHead, aLine[1:].strip(), None, sAlign self.T_COMMENT, nHead, cText, None, sAlign
)) ))
if self._doComments and self._keepMarkdown: if self._doComments and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
+4
View File
@@ -170,6 +170,10 @@ class ToMarkdown(Tokenizer):
label = self._localLookup("Synopsis") label = self._localLookup("Synopsis")
lines.append(f"**{label}:** {tText}\n\n") lines.append(f"**{label}:** {tText}\n\n")
elif tType == self.T_BRIEF and self._doSynopsis:
label = self._localLookup("Brief")
lines.append(f"**{label}:** {tText}\n\n")
elif tType == self.T_COMMENT and self._doComments: elif tType == self.T_COMMENT and self._doComments:
label = self._localLookup("Comment") label = self._localLookup("Comment")
lines.append(f"**{label}:** {tText}\n\n") lines.append(f"**{label}:** {tText}\n\n")
+7 -3
View File
@@ -481,7 +481,11 @@ class ToOdt(Tokenizer):
pFmt.append(tFormat) pFmt.append(tFormat)
elif tType == self.T_SYNOPSIS and self._doSynopsis: elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText) tTemp, fTemp = self._formatSynopsis(tText, True)
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
elif tType == self.T_BRIEF and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText, False)
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp) self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
elif tType == self.T_COMMENT and self._doComments: elif tType == self.T_COMMENT and self._doComments:
@@ -552,9 +556,9 @@ class ToOdt(Tokenizer):
# Internal Functions # Internal Functions
## ##
def _formatSynopsis(self, text: str) -> tuple[str, list[tuple[int, int]]]: def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to synopsis lines.""" """Apply formatting to synopsis lines."""
name = self._localLookup("Synopsis") name = self._localLookup("Synopsis") if synopsis else self._localLookup("Brief")
rTxt = f"{name}: {text}" rTxt = f"{name}: {text}"
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)] rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt return rTxt, rFmt
+9
View File
@@ -61,6 +61,15 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout # END Enum nwItemLayout
class nwComment(Enum):
PLAIN = 0
SYNOPSIS = 1
BRIEF = 2
# END Enum nwComment
class nwTrinary(Enum): class nwTrinary(Enum):
NEGATIVE = -1 NEGATIVE = -1
+7 -9
View File
@@ -36,6 +36,8 @@ from PyQt5.QtGui import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
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.enum import nwComment
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -352,16 +354,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
elif text.startswith("%"): # Comments elif text.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT) self.setCurrentBlockState(self.BLOCK_TEXT)
toCheck = text[1:].lstrip() cStyle, _, cPos = processComment(text)
synTag = toCheck[:9].lower() if cStyle == nwComment.PLAIN:
tLen = len(text) self.setFormat(0, len(text), self._hStyles["hidden"])
cLen = len(toCheck)
cOff = tLen - cLen
if synTag == "synopsis:":
self.setFormat(0, cOff+9, self._hStyles["modifier"])
self.setFormat(cOff+9, tLen, self._hStyles["hidden"])
else: else:
self.setFormat(0, tLen, self._hStyles["hidden"]) self.setFormat(0, cPos, self._hStyles["modifier"])
self.setFormat(cPos, len(text), self._hStyles["hidden"])
else: # Text Paragraph else: # Text Paragraph
+61 -4
View File
@@ -34,9 +34,10 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
from novelwriter.core.index import IndexHeading, IndexItem from novelwriter.core.index import IndexHeading, IndexItem
from novelwriter.enum import nwDocMode, nwItemClass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -103,6 +104,23 @@ class GuiDocViewerPanel(QWidget):
return return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
widths = SHARED.project.options.getValue("GuiDocViewerPanel", "colWidths", {})
if isinstance(widths, dict):
for key, value in widths.items():
if key in self.kwTabs and isinstance(value, list):
self.kwTabs[key].setColumnWidths(value)
return
def closeProjectTasks(self) -> None:
"""Run close project tasks."""
widths = {}
for key, tab in self.kwTabs.items():
widths[key] = tab.getColumnWidths()
SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", widths)
return
## ##
# Public Slots # Public Slots
## ##
@@ -213,6 +231,7 @@ class _ViewPanelBackRefs(QTreeWidget):
# Signals # Signals
self.clicked.connect(self._treeItemClicked) self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked)
return return
@@ -253,6 +272,14 @@ class _ViewPanelBackRefs(QTreeWidget):
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True) self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return return
@pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
"""Emit follow tag signal on user double click."""
tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
if index.column() == self.C_DOC:
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -295,6 +322,7 @@ class _ViewPanelKeyWords(QTreeWidget):
C_VIEW = 2 C_VIEW = 2
C_DOC = 3 C_DOC = 3
C_TITLE = 4 C_TITLE = 4
C_BRIEF = 5
D_TAG = Qt.ItemDataRole.UserRole D_TAG = Qt.ItemDataRole.UserRole
@@ -307,11 +335,16 @@ class _ViewPanelKeyWords(QTreeWidget):
iPx = SHARED.theme.baseIconSize iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6) cMg = CONFIG.pxInt(6)
self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Document"), self.tr("Heading")]) self.setHeaderLabels([
self.tr("Tag"), "", "", self.tr("Document"),
self.tr("Heading"), self.tr("Brief")
])
self.setIndentation(0) self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setFrameStyle(QFrame.Shape.NoFrame) self.setFrameStyle(QFrame.Shape.NoFrame)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder) self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder)
@@ -321,9 +354,9 @@ class _ViewPanelKeyWords(QTreeWidget):
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents) treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed) treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed) treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.resizeSection(self.C_EDIT, iPx + cMg) treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg) treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
treeHeader.setSectionsMovable(False)
# Cache Icons Locally # Cache Icons Locally
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]) self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])
@@ -332,6 +365,7 @@ class _ViewPanelKeyWords(QTreeWidget):
# Signals # Signals
self.clicked.connect(self._treeItemClicked) self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked)
return return
@@ -367,6 +401,7 @@ class _ViewPanelKeyWords(QTreeWidget):
trItem.setText(self.C_DOC, nwItem.itemName) trItem.setText(self.C_DOC, nwItem.itemName)
trItem.setText(self.C_TITLE, hItem.title) trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec) trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
trItem.setText(self.C_BRIEF, hItem.synopsis)
trItem.setData(self.C_DATA, self.D_TAG, tag) trItem.setData(self.C_DATA, self.D_TAG, tag)
if tag not in self._treeMap: if tag not in self._treeMap:
@@ -383,6 +418,20 @@ class _ViewPanelKeyWords(QTreeWidget):
return True return True
return False return False
def setColumnWidths(self, widths: list[int]) -> None:
"""Set the column widths."""
if isinstance(widths, list) and len(widths) >= 2:
self.setColumnWidth(self.C_DOC, CONFIG.pxInt(checkInt(widths[0], 100)))
self.setColumnWidth(self.C_TITLE, CONFIG.pxInt(checkInt(widths[1], 100)))
return
def getColumnWidths(self) -> list[int]:
"""Get the widths of the user-adjustable columns."""
return [
CONFIG.rpxInt(self.columnWidth(self.C_DOC)),
CONFIG.rpxInt(self.columnWidth(self.C_TITLE)),
]
## ##
# Private Slots # Private Slots
## ##
@@ -397,4 +446,12 @@ class _ViewPanelKeyWords(QTreeWidget):
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW) self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return return
@pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
"""Emit follow tag signal on user double click."""
tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
if index.column() == self.C_NAME:
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
# END Class _ViewPanelKeyWords # END Class _ViewPanelKeyWords
+2
View File
@@ -454,6 +454,7 @@ class GuiMain(QMainWindow):
self.docViewer.clearNavHistory() self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False) self.closeDocViewer(byUser=False)
self.docViewerPanel.closeProjectTasks()
self.outlineView.closeProjectTasks() self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks() self.novelView.closeProjectTasks()
self.projView.clearProjectView() self.projView.clearProjectView()
@@ -527,6 +528,7 @@ class GuiMain(QMainWindow):
self.projView.openProjectTasks() self.projView.openProjectTasks()
self.novelView.openProjectTasks() self.novelView.openProjectTasks()
self.outlineView.openProjectTasks() self.outlineView.openProjectTasks()
self.docViewerPanel.openProjectTasks()
self._updateStatusWordCount() self._updateStatusWordCount()
# Restore previously open documents, if any # Restore previously open documents, if any
+4 -2
View File
@@ -1,10 +1,12 @@
%%~name: John Smith %%~name: John Smith
%%~path: f7e2d9f330615/14298de4d9524 %%~path: f7e2d9f330615/14298de4d9524
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
%%~hash: 259eff30f10e101cf93e27e8764e851d356d54b8 %%~hash: fda91c416d874aa41a47fceaedb3d62f040c7e32
%%~date: Unknown/2023-08-25 16:52:03 %%~date: Unknown/2023-11-25 18:16:13
# John Smith # John Smith
@tag: John @tag: John
% Brief: The sidekick
Hes pretty cool. Not Brad Pitt though. Hes pretty cool. Not Brad Pitt though.
+4 -2
View File
@@ -1,10 +1,12 @@
%%~name: Jane Smith %%~name: Jane Smith
%%~path: f7e2d9f330615/bb2c23b3c42cc %%~path: f7e2d9f330615/bb2c23b3c42cc
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
%%~hash: a69e4ca6ceede2536c8d364f88e6b62ef0ec36c3 %%~hash: b7291713899bd0356617a36ae606b08e5fee1b65
%%~date: Unknown/2023-08-25 16:52:04 %%~date: Unknown/2023-11-25 18:16:07
# Jane Smith # Jane Smith
@tag: Jane @tag: Jane
% Brief: The heroine
Shes pretty cool. Not Angelina Jolie though. Shes pretty cool. Not Angelina Jolie though.
+4 -4
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.2-beta1" hexVersion="0x020200b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-11-11 22:48:30"> <novelWriterXML appVersion="2.2-beta1" hexVersion="0x020200b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-11-25 18:16:17">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1611" autoCount="255" editTime="81193"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1612" autoCount="256" editTime="81268">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
@@ -102,11 +102,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="65" /> <meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="48" />
<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="71" /> <meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="47" />
<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">
+31 -4
View File
@@ -29,9 +29,9 @@ from mocked import causeException
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from tools import C, buildTestProject, cmpFiles, writeFile from tools import C, buildTestProject, cmpFiles, writeFile
from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.enum import nwComment, nwItemClass, nwItemLayout
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex, processComment
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
@@ -1264,7 +1264,34 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_CountWords(): def testCoreIndex_processComment():
"""Test the comment processing function."""
# Regular comment
assert processComment("%Hi") == (nwComment.PLAIN, "Hi", 0)
assert processComment("% Hi") == (nwComment.PLAIN, "Hi", 0)
assert processComment("% Hi:you") == (nwComment.PLAIN, "Hi:you", 0)
# Synopsis
assert processComment("%synopsis:") == (nwComment.PLAIN, "synopsis:", 0)
assert processComment("%synopsis: Hi") == (nwComment.SYNOPSIS, "Hi", 10)
assert processComment("% synopsis: Hi") == (nwComment.SYNOPSIS, "Hi", 11)
assert processComment("% synopsis : Hi") == (nwComment.SYNOPSIS, "Hi", 13)
assert processComment("% Synopsis : Hi") == (nwComment.SYNOPSIS, "Hi", 15)
assert processComment("% \t SYNOPSIS : Hi") == (nwComment.SYNOPSIS, "Hi", 16)
# Brief
assert processComment("%brief:") == (nwComment.PLAIN, "brief:", 0)
assert processComment("%brief: Hi") == (nwComment.BRIEF, "Hi", 7)
assert processComment("% brief: Hi") == (nwComment.BRIEF, "Hi", 8)
assert processComment("% brief : Hi") == (nwComment.BRIEF, "Hi", 10)
assert processComment("% Brief : Hi") == (nwComment.BRIEF, "Hi", 12)
assert processComment("% \t BRIEF : Hi") == (nwComment.BRIEF, "Hi", 13)
# END Test testCoreIndex_processComment
@pytest.mark.core
def testCoreIndex_countWords():
"""Test the word counter and the exclusion filers.""" """Test the word counter and the exclusion filers."""
# Non-Text # Non-Text
assert countWords(None) == (0, 0, 0) # type: ignore assert countWords(None) == (0, 0, 0) # type: ignore
@@ -1362,4 +1389,4 @@ def testCoreIndex_CountWords():
assert wC == 14 assert wC == 14
assert pC == 2 assert pC == 2
# END Test testCoreIndex_CountWords # END Test testCoreIndex_countWords
+17 -3
View File
@@ -149,7 +149,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
"<p class='break'>Line one<br/>Line two<br/>Line three</p>\n" "<p class='break'>Line one<br/>Line two<br/>Line three</p>\n"
) )
# Synopsis # Synopsis, Brief
html._text = "%synopsis: The synopsis ...\n" html._text = "%synopsis: The synopsis ...\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
@@ -163,6 +163,14 @@ def testCoreToHtml_ConvertFormat(mockGUI):
"<p class='synopsis'><strong>Synopsis:</strong> The synopsis ...</p>\n" "<p class='synopsis'><strong>Synopsis:</strong> The synopsis ...</p>\n"
) )
html.setSynopsis(True)
html._text = "%brief: A description ...\n"
html.tokenizeText()
html.doConvert()
assert html.theResult == (
"<p class='synopsis'><strong>Brief:</strong> A description ...</p>\n"
)
# Comment # Comment
html._text = "% A comment ...\n" html._text = "% A comment ...\n"
html.tokenizeText() html.tokenizeText()
@@ -610,9 +618,12 @@ def testCoreToHtml_Format(mockGUI):
# Export Mode # Export Mode
# =========== # ===========
assert html._formatSynopsis("synopsis text") == ( assert html._formatSynopsis("synopsis text", True) == (
"<p class='synopsis'><strong>Synopsis:</strong> synopsis text</p>\n" "<p class='synopsis'><strong>Synopsis:</strong> synopsis text</p>\n"
) )
assert html._formatSynopsis("brief text", False) == (
"<p class='synopsis'><strong>Brief:</strong> brief text</p>\n"
)
assert html._formatComments("comment text") == ( assert html._formatComments("comment text") == (
"<p class='comment'><strong>Comment:</strong> comment text</p>\n" "<p class='comment'><strong>Comment:</strong> comment text</p>\n"
) )
@@ -632,9 +643,12 @@ def testCoreToHtml_Format(mockGUI):
html.setPreview(True, True) html.setPreview(True, True)
assert html._formatSynopsis("synopsis text") == ( assert html._formatSynopsis("synopsis text", True) == (
"<p class='comment'><span class='synopsis'>Synopsis:</span> synopsis text</p>\n" "<p class='comment'><span class='synopsis'>Synopsis:</span> synopsis text</p>\n"
) )
assert html._formatSynopsis("brief text", False) == (
"<p class='comment'><span class='synopsis'>Brief:</span> brief text</p>\n"
)
assert html._formatComments("comment text") == ( assert html._formatComments("comment text") == (
"<p class='comment'>comment text</p>\n" "<p class='comment'>comment text</p>\n"
) )
+14
View File
@@ -471,6 +471,20 @@ def testCoreToken_MetaFormat(mockGUI):
tokens.tokenizeText() tokens.tokenizeText()
assert tokens.theMarkdown[-1] == "% synopsis: The synopsis\n\n" assert tokens.theMarkdown[-1] == "% synopsis: The synopsis\n\n"
# Brief
tokens.setSynopsis(False)
tokens._text = "% brief: A description\n"
tokens.tokenizeText()
assert tokens._tokens == [
(Tokenizer.T_BRIEF, 0, "A description", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE),
]
assert tokens.theMarkdown[-1] == "\n"
tokens.setSynopsis(True)
tokens.tokenizeText()
assert tokens.theMarkdown[-1] == "% brief: A description\n\n"
# Keyword # Keyword
tokens._text = "@char: Bod\n" tokens._text = "@char: Bod\n"
tokens.tokenizeText() tokens.tokenizeText()
+7 -1
View File
@@ -106,7 +106,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
theMD.doConvert() theMD.doConvert()
assert theMD.theResult == "Line one \nLine two \nLine three\n\n" assert theMD.theResult == "Line one \nLine two \nLine three\n\n"
# Synopsis # Synopsis, Brief
theMD._text = "%synopsis: The synopsis ...\n" theMD._text = "%synopsis: The synopsis ...\n"
theMD.tokenizeText() theMD.tokenizeText()
theMD.doConvert() theMD.doConvert()
@@ -118,6 +118,12 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
theMD.doConvert() theMD.doConvert()
assert theMD.theResult == "**Synopsis:** The synopsis ...\n\n" assert theMD.theResult == "**Synopsis:** The synopsis ...\n\n"
theMD.setSynopsis(True)
theMD._text = "%brief: A description ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Brief:** A description ...\n\n"
# Comment # Comment
theMD._text = "% A comment ...\n" theMD._text = "% A comment ...\n"
theMD.tokenizeText() theMD.tokenizeText()
+10 -4
View File
@@ -447,12 +447,13 @@ def testCoreToOdt_Convert(mockGUI):
'</office:text>' '</office:text>'
) )
# Synopsis, Comment, Keywords # Synopsis, Brief, Comment, Keywords
odt._text = ( odt._text = (
"### Scene\n\n" "### Scene\n\n"
"@pov: Jane\n\n" "@pov: Jane\n\n"
"% synopsis: So it begins\n\n" "% synopsis: So it begins\n\n"
"% a plain comment\n\n" "% brief: Then what\n\n"
"% A plain comment\n\n"
) )
odt.setSynopsis(True) odt.setSynopsis(True)
odt.setComments(True) odt.setComments(True)
@@ -470,7 +471,9 @@ def testCoreToOdt_Convert(mockGUI):
'<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T7">' '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T7">'
'Synopsis:</text:span> So it begins</text:p>' 'Synopsis:</text:span> So it begins</text:p>'
'<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T7">' '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T7">'
'Comment:</text:span> a plain comment</text:p>' 'Brief:</text:span> Then what</text:p>'
'<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T7">'
'Comment:</text:span> A plain comment</text:p>'
'</office:text>' '</office:text>'
) )
@@ -804,9 +807,12 @@ def testCoreToOdt_Format(mockGUI):
project = NWProject() project = NWProject()
odt = ToOdt(project, isFlat=True) odt = ToOdt(project, isFlat=True)
assert odt._formatSynopsis("synopsis text") == ( assert odt._formatSynopsis("synopsis text", True) == (
"Synopsis: synopsis text", [(0, ToOdt.FMT_B_B), (9, ToOdt.FMT_B_E)] "Synopsis: synopsis text", [(0, ToOdt.FMT_B_B), (9, ToOdt.FMT_B_E)]
) )
assert odt._formatSynopsis("brief text", False) == (
"Brief: brief text", [(0, ToOdt.FMT_B_B), (6, ToOdt.FMT_B_E)]
)
assert odt._formatComments("comment text") == ( assert odt._formatComments("comment text") == (
"Comment: comment text", [(0, ToOdt.FMT_B_B), (8, ToOdt.FMT_B_E)] "Comment: comment text", [(0, ToOdt.FMT_B_B), (8, ToOdt.FMT_B_E)]
) )
@@ -102,6 +102,12 @@ def testGuiViewerPanel_BackRefs(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
tabBackRefs._treeItemClicked(tabBackRefs.model().index(0, tabBackRefs.C_VIEW)) tabBackRefs._treeItemClicked(tabBackRefs.model().index(0, tabBackRefs.C_VIEW))
assert nwGUI.docViewer.docHandle == C.hSceneDoc assert nwGUI.docViewer.docHandle == C.hSceneDoc
# Double-Click
nwGUI.viewDocument(hJane)
assert nwGUI.docViewer.docHandle == hJane
tabBackRefs._treeItemDoubleClicked(tabBackRefs.model().index(0, tabBackRefs.C_DOC))
assert nwGUI.docViewer.docHandle == C.hSceneDoc
# qtbot.stop() # qtbot.stop()
# END Test testGuiViewerPanel_BackRefs # END Test testGuiViewerPanel_BackRefs
@@ -189,6 +195,8 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd
assert nwGUI.docViewer.docHandle == C.hSceneDoc assert nwGUI.docViewer.docHandle == C.hSceneDoc
charTab._treeItemClicked(charTab.model().index(1, charTab.C_VIEW)) charTab._treeItemClicked(charTab.model().index(1, charTab.C_VIEW))
assert nwGUI.docViewer.docHandle == hJohn assert nwGUI.docViewer.docHandle == hJohn
charTab._treeItemDoubleClicked(charTab.model().index(0, charTab.C_NAME))
assert nwGUI.docViewer.docHandle == hJane
# qtbot.stop() # qtbot.stop()