novelWriter 2.2b1__py3-none-any.whl → 2.2rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +60 -48
- novelwriter/__init__.py +3 -3
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/novelwriter.ico +0 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -1
- novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -1
- novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
- novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/text/release_notes.htm +4 -4
- novelwriter/common.py +22 -1
- novelwriter/config.py +12 -27
- novelwriter/constants.py +20 -3
- novelwriter/core/buildsettings.py +1 -1
- novelwriter/core/coretools.py +6 -1
- novelwriter/core/index.py +100 -34
- novelwriter/core/options.py +3 -0
- novelwriter/core/project.py +2 -2
- novelwriter/core/projectdata.py +1 -1
- novelwriter/core/tohtml.py +9 -3
- novelwriter/core/tokenizer.py +27 -20
- novelwriter/core/tomd.py +4 -0
- novelwriter/core/toodt.py +11 -4
- novelwriter/dialogs/preferences.py +80 -82
- novelwriter/dialogs/updates.py +25 -14
- novelwriter/enum.py +14 -4
- novelwriter/gui/doceditor.py +282 -177
- novelwriter/gui/dochighlight.py +7 -9
- novelwriter/gui/docviewer.py +142 -319
- novelwriter/gui/docviewerpanel.py +457 -0
- novelwriter/gui/editordocument.py +1 -1
- novelwriter/gui/mainmenu.py +16 -7
- novelwriter/gui/outline.py +10 -6
- novelwriter/gui/projtree.py +461 -376
- novelwriter/gui/sidebar.py +3 -3
- novelwriter/gui/statusbar.py +1 -1
- novelwriter/gui/theme.py +21 -2
- novelwriter/guimain.py +86 -32
- novelwriter/shared.py +23 -1
- novelwriter/tools/dictionaries.py +268 -0
- novelwriter/tools/manusbuild.py +17 -6
- novelwriter/tools/manuscript.py +1 -1
- novelwriter/tools/writingstats.py +1 -1
- novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
novelwriter/constants.py
CHANGED
@@ -50,6 +50,9 @@ class nwConst:
|
|
50
50
|
URL_HELP = "https://github.com/vkbo/novelWriter/discussions"
|
51
51
|
URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest"
|
52
52
|
|
53
|
+
# Requests
|
54
|
+
USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
|
55
|
+
|
53
56
|
# Gui Settings
|
54
57
|
STATUS_MSG_TIMEOUT = 15000 # milliseconds
|
55
58
|
|
@@ -153,6 +156,21 @@ class nwKeyWords:
|
|
153
156
|
# END Class nwKeyWords
|
154
157
|
|
155
158
|
|
159
|
+
class nwLists:
|
160
|
+
|
161
|
+
USER_CLASSES = [
|
162
|
+
nwItemClass.CHARACTER,
|
163
|
+
nwItemClass.PLOT,
|
164
|
+
nwItemClass.WORLD,
|
165
|
+
nwItemClass.TIMELINE,
|
166
|
+
nwItemClass.OBJECT,
|
167
|
+
nwItemClass.ENTITY,
|
168
|
+
nwItemClass.CUSTOM,
|
169
|
+
]
|
170
|
+
|
171
|
+
# END Class nwLists
|
172
|
+
|
173
|
+
|
156
174
|
class nwLabels:
|
157
175
|
|
158
176
|
CLASS_NAME = {
|
@@ -221,8 +239,8 @@ class nwLabels:
|
|
221
239
|
nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
|
222
240
|
nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
|
223
241
|
nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
|
224
|
-
nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
|
225
242
|
nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
|
243
|
+
nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
|
226
244
|
nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
|
227
245
|
nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
|
228
246
|
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
@@ -327,8 +345,7 @@ class nwQuotes:
|
|
327
345
|
|
328
346
|
|
329
347
|
class nwUnicode:
|
330
|
-
"""Supported unicode character constants and their HTML equivalents.
|
331
|
-
"""
|
348
|
+
"""Supported unicode character constants and their HTML equivalents."""
|
332
349
|
# Unicode Constants
|
333
350
|
# =================
|
334
351
|
|
@@ -349,7 +349,7 @@ class BuildSettings:
|
|
349
349
|
def buildItemFilter(
|
350
350
|
self, project: NWProject, withRoots: bool = False
|
351
351
|
) -> dict[str, tuple[bool, FilterMode]]:
|
352
|
-
"""Return a dictionary of item handles with filter
|
352
|
+
"""Return a dictionary of item handles with filter decisions
|
353
353
|
applied.
|
354
354
|
"""
|
355
355
|
result: dict[str, tuple[bool, FilterMode]] = {}
|
novelwriter/core/coretools.py
CHANGED
@@ -406,6 +406,7 @@ class ProjectBuilder:
|
|
406
406
|
|
407
407
|
chSynop = self.tr("Summary of the chapter.")
|
408
408
|
scSynop = self.tr("Summary of the scene.")
|
409
|
+
bfNote = self.tr("A short description.")
|
409
410
|
|
410
411
|
# Create chapters
|
411
412
|
if numChapters > 0:
|
@@ -446,7 +447,11 @@ class ProjectBuilder:
|
|
446
447
|
aHandle = project.newFile(noteTitles[newRoot], rHandle)
|
447
448
|
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
|
448
449
|
aDoc = project.storage.getDocument(aHandle)
|
449
|
-
aDoc.writeDocument(
|
450
|
+
aDoc.writeDocument(
|
451
|
+
f"# {noteTitles[newRoot]}\n\n"
|
452
|
+
f"@tag: {ntTag}\n\n"
|
453
|
+
f"% Short: {bfNote}\n\n"
|
454
|
+
)
|
450
455
|
|
451
456
|
# Also add the archive and trash folders
|
452
457
|
project.newRoot(nwItemClass.ARCHIVE)
|
novelwriter/core/index.py
CHANGED
@@ -35,7 +35,8 @@ from time import time
|
|
35
35
|
from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
|
36
36
|
from pathlib import Path
|
37
37
|
|
38
|
-
from novelwriter
|
38
|
+
from novelwriter import SHARED
|
39
|
+
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
|
39
40
|
from novelwriter.error import logException
|
40
41
|
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
41
42
|
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
|
@@ -112,6 +113,7 @@ class NWIndex:
|
|
112
113
|
self._itemIndex.clear()
|
113
114
|
self._indexChange = 0.0
|
114
115
|
self._rootChange = {}
|
116
|
+
SHARED.indexSignalProxy({"event": "clearIndex"})
|
115
117
|
return
|
116
118
|
|
117
119
|
def rebuildIndex(self) -> None:
|
@@ -121,16 +123,22 @@ class NWIndex:
|
|
121
123
|
if nwItem.isFileType():
|
122
124
|
tHandle = nwItem.itemHandle
|
123
125
|
theDoc = self._project.storage.getDocument(tHandle)
|
124
|
-
self.scanText(tHandle, theDoc.readDocument() or "")
|
126
|
+
self.scanText(tHandle, theDoc.readDocument() or "", blockSignal=True)
|
125
127
|
self._indexBroken = False
|
128
|
+
SHARED.indexSignalProxy({"event": "buildIndex"})
|
126
129
|
return
|
127
130
|
|
128
131
|
def deleteHandle(self, tHandle: str) -> None:
|
129
132
|
"""Delete all entries of a given document handle."""
|
130
133
|
logger.debug("Removing item '%s' from the index", tHandle)
|
131
|
-
|
134
|
+
delTags = self._itemIndex.allItemTags(tHandle)
|
135
|
+
for tTag in delTags:
|
132
136
|
del self._tagsIndex[tTag]
|
133
137
|
del self._itemIndex[tHandle]
|
138
|
+
SHARED.indexSignalProxy({
|
139
|
+
"event": "updateTags",
|
140
|
+
"deleted": delTags,
|
141
|
+
})
|
134
142
|
return
|
135
143
|
|
136
144
|
def reIndexHandle(self, tHandle: str | None) -> bool:
|
@@ -138,14 +146,12 @@ class NWIndex:
|
|
138
146
|
moved from the archive or trash folders back into the active
|
139
147
|
project.
|
140
148
|
"""
|
141
|
-
if tHandle
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
return True
|
149
|
+
if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
|
150
|
+
logger.debug("Re-indexing item '%s'", tHandle)
|
151
|
+
theDoc = self._project.storage.getDocument(tHandle)
|
152
|
+
self.scanText(tHandle, theDoc.readDocument() or "")
|
153
|
+
return True
|
154
|
+
return False
|
149
155
|
|
150
156
|
def indexChangedSince(self, checkTime: int | float) -> bool:
|
151
157
|
"""Check if the index has changed since a given time."""
|
@@ -200,6 +206,7 @@ class NWIndex:
|
|
200
206
|
self.reIndexHandle(fHandle)
|
201
207
|
|
202
208
|
self._indexChange = time()
|
209
|
+
SHARED.indexSignalProxy({"event": "buildIndex"})
|
203
210
|
|
204
211
|
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
|
205
212
|
|
@@ -238,7 +245,7 @@ class NWIndex:
|
|
238
245
|
# Index Building
|
239
246
|
##
|
240
247
|
|
241
|
-
def scanText(self, tHandle: str, text: str) -> bool:
|
248
|
+
def scanText(self, tHandle: str, text: str, blockSignal: bool = False) -> bool:
|
242
249
|
"""Scan a piece of text associated with a handle. This will
|
243
250
|
update the indices accordingly. This function takes the handle
|
244
251
|
and text as separate inputs as we want to primarily scan the
|
@@ -282,6 +289,11 @@ class NWIndex:
|
|
282
289
|
nowTime = time()
|
283
290
|
self._indexChange = nowTime
|
284
291
|
self._rootChange[tItem.itemRoot] = nowTime
|
292
|
+
if not blockSignal:
|
293
|
+
SHARED.indexSignalProxy({
|
294
|
+
"event": "scanText",
|
295
|
+
"handle": tHandle,
|
296
|
+
})
|
285
297
|
|
286
298
|
return True
|
287
299
|
|
@@ -289,7 +301,7 @@ class NWIndex:
|
|
289
301
|
# Internal Indexer Helpers
|
290
302
|
##
|
291
303
|
|
292
|
-
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict) -> None:
|
304
|
+
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None:
|
293
305
|
"""Scan an active document for meta data."""
|
294
306
|
nTitle = 0 # Line Number of the previous title
|
295
307
|
cTitle = TT_NONE # Tag of the current title
|
@@ -326,14 +338,9 @@ class NWIndex:
|
|
326
338
|
|
327
339
|
elif line.startswith("%"):
|
328
340
|
if cTitle != TT_NONE:
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
cLen = len(toCheck)
|
333
|
-
cOff = tLen - cLen
|
334
|
-
if synTag == "synopsis:":
|
335
|
-
sText = line[cOff+9:].strip()
|
336
|
-
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
|
341
|
+
cStyle, cText, _ = processComment(line)
|
342
|
+
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
|
343
|
+
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
|
337
344
|
|
338
345
|
# Count words for remaining text after last heading
|
339
346
|
if pTitle != TT_NONE:
|
@@ -346,9 +353,21 @@ class NWIndex:
|
|
346
353
|
|
347
354
|
# Prune no longer used tags
|
348
355
|
for tTag, isActive in tags.items():
|
349
|
-
|
350
|
-
|
356
|
+
updated = []
|
357
|
+
deleted = []
|
358
|
+
if isActive:
|
359
|
+
logger.debug("Added/updated tag '%s'", tTag)
|
360
|
+
updated.append(tTag)
|
361
|
+
else:
|
362
|
+
logger.debug("Removed tag '%s'", tTag)
|
351
363
|
del self._tagsIndex[tTag]
|
364
|
+
deleted.append(tTag)
|
365
|
+
if updated or deleted:
|
366
|
+
SHARED.indexSignalProxy({
|
367
|
+
"event": "updateTags",
|
368
|
+
"updated": updated,
|
369
|
+
"deleted": deleted,
|
370
|
+
})
|
352
371
|
|
353
372
|
return
|
354
373
|
|
@@ -385,7 +404,7 @@ class NWIndex:
|
|
385
404
|
return
|
386
405
|
|
387
406
|
def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
|
388
|
-
itemClass: nwItemClass, tags: dict) -> None:
|
407
|
+
itemClass: nwItemClass, tags: dict[str, bool]) -> None:
|
389
408
|
"""Validate and save the information about a reference to a tag
|
390
409
|
in another file, or the setting of a tag in the file. A record
|
391
410
|
of active tags is updated so that no longer used tags can be
|
@@ -596,10 +615,8 @@ class NWIndex:
|
|
596
615
|
|
597
616
|
return tRefs
|
598
617
|
|
599
|
-
def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
|
600
|
-
"""Build a
|
601
|
-
by tHandle.
|
602
|
-
"""
|
618
|
+
def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
|
619
|
+
"""Build a dict of files referring back to our file."""
|
603
620
|
if tHandle is None or tHandle not in self._itemIndex:
|
604
621
|
return {}
|
605
622
|
|
@@ -611,20 +628,43 @@ class NWIndex:
|
|
611
628
|
for aHandle, sTitle, hItem in self._itemIndex.iterAllHeaders():
|
612
629
|
for aTag in hItem.references:
|
613
630
|
if aTag in tTags and aHandle not in tRefs:
|
614
|
-
tRefs[aHandle] = sTitle
|
631
|
+
tRefs[aHandle] = (sTitle, hItem)
|
615
632
|
|
616
633
|
return tRefs
|
617
634
|
|
618
|
-
def getTagSource(self, tagKey: str) -> tuple[str, str]:
|
635
|
+
def getTagSource(self, tagKey: str) -> tuple[str | None, str]:
|
619
636
|
"""Return the source location of a given tag."""
|
620
637
|
tHandle = self._tagsIndex.tagHandle(tagKey)
|
621
638
|
sTitle = self._tagsIndex.tagHeading(tagKey)
|
622
639
|
return tHandle, sTitle
|
623
640
|
|
624
|
-
def
|
641
|
+
def getDocumentTags(self, tHandle: str | None) -> list[str]:
|
642
|
+
"""Return all tags used by a specific document."""
|
643
|
+
return self._itemIndex.allItemTags(tHandle) if tHandle else []
|
644
|
+
|
645
|
+
def getClassTags(self, itemClass: nwItemClass) -> list[str]:
|
625
646
|
"""Return all tags based on itemClass."""
|
626
647
|
return self._tagsIndex.filterTagNames(itemClass.name)
|
627
648
|
|
649
|
+
def getTagsData(self) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
|
650
|
+
"""Return all known tags."""
|
651
|
+
for tag, data in self._tagsIndex.items():
|
652
|
+
iItem = self._itemIndex[data.get("handle")]
|
653
|
+
hItem = None if iItem is None else iItem[data.get("heading")]
|
654
|
+
yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem
|
655
|
+
return
|
656
|
+
|
657
|
+
def getSingleTag(self, tagKey: str) -> tuple[str, str, IndexItem | None, IndexHeading | None]:
|
658
|
+
"""Return tag data for a specific tag."""
|
659
|
+
tName = self._tagsIndex.tagName(tagKey)
|
660
|
+
tClass = self._tagsIndex.tagClass(tagKey)
|
661
|
+
tHandle = self._tagsIndex.tagHandle(tagKey)
|
662
|
+
tHeading = self._tagsIndex.tagHeading(tagKey)
|
663
|
+
if tName and tClass and tHandle and tHeading:
|
664
|
+
iItem = self._itemIndex[tHandle]
|
665
|
+
return tName, tClass, iItem, None if iItem is None else iItem[tHeading]
|
666
|
+
return "", "", None, None
|
667
|
+
|
628
668
|
# END Class NWIndex
|
629
669
|
|
630
670
|
|
@@ -643,7 +683,7 @@ class TagsIndex:
|
|
643
683
|
__slots__ = ("_tags")
|
644
684
|
|
645
685
|
def __init__(self) -> None:
|
646
|
-
self._tags: dict[str, dict] = {}
|
686
|
+
self._tags: dict[str, dict[str, str]] = {}
|
647
687
|
return
|
648
688
|
|
649
689
|
def __contains__(self, tagKey: str) -> bool:
|
@@ -665,6 +705,10 @@ class TagsIndex:
|
|
665
705
|
self._tags = {}
|
666
706
|
return
|
667
707
|
|
708
|
+
def items(self) -> ItemsView:
|
709
|
+
"""Return a dictionary view of all tags."""
|
710
|
+
return self._tags.items()
|
711
|
+
|
668
712
|
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None:
|
669
713
|
"""Add a key to the index and set all values."""
|
670
714
|
self._tags[tagKey.lower()] = {
|
@@ -676,7 +720,7 @@ class TagsIndex:
|
|
676
720
|
"""Get the display name of a given tag."""
|
677
721
|
return self._tags.get(tagKey.lower(), {}).get("name", "")
|
678
722
|
|
679
|
-
def tagHandle(self, tagKey: str) -> str:
|
723
|
+
def tagHandle(self, tagKey: str) -> str | None:
|
680
724
|
"""Get the handle of a given tag."""
|
681
725
|
return self._tags.get(tagKey.lower(), {}).get("handle", None)
|
682
726
|
|
@@ -937,6 +981,11 @@ class IndexItem:
|
|
937
981
|
# Properties
|
938
982
|
##
|
939
983
|
|
984
|
+
@property
|
985
|
+
def handle(self) -> str:
|
986
|
+
"""Return the item handle of the index item."""
|
987
|
+
return self._handle
|
988
|
+
|
940
989
|
@property
|
941
990
|
def item(self) -> NWItem:
|
942
991
|
"""Return the project item of the index item."""
|
@@ -1215,9 +1264,26 @@ class IndexHeading:
|
|
1215
1264
|
|
1216
1265
|
|
1217
1266
|
# =============================================================================================== #
|
1218
|
-
#
|
1267
|
+
# Text Processing Functions
|
1219
1268
|
# =============================================================================================== #
|
1220
1269
|
|
1270
|
+
CLASSIFIERS = {
|
1271
|
+
"short": nwComment.SHORT,
|
1272
|
+
"synopsis": nwComment.SYNOPSIS,
|
1273
|
+
}
|
1274
|
+
|
1275
|
+
|
1276
|
+
def processComment(text: str) -> tuple[nwComment, str, int]:
|
1277
|
+
"""Extract comment style and text. Should only be called on text
|
1278
|
+
starting with a %.
|
1279
|
+
"""
|
1280
|
+
check = text[1:].lstrip()
|
1281
|
+
classifier, _, content = check.partition(":")
|
1282
|
+
if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
|
1283
|
+
return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
|
1284
|
+
return nwComment.PLAIN, check, 0
|
1285
|
+
|
1286
|
+
|
1221
1287
|
def countWords(text: str) -> tuple[int, int, int]:
|
1222
1288
|
"""Count words in a piece of text, skipping special syntax and
|
1223
1289
|
comments.
|
novelwriter/core/options.py
CHANGED
novelwriter/core/project.py
CHANGED
@@ -45,7 +45,7 @@ from novelwriter.core.sessions import NWSessionLog
|
|
45
45
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
46
46
|
from novelwriter.core.projectdata import NWProjectData
|
47
47
|
from novelwriter.common import (
|
48
|
-
checkStringNone, formatInt, formatTimeStamp, hexToInt, makeFileNameSafe, minmax
|
48
|
+
checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt, makeFileNameSafe, minmax
|
49
49
|
)
|
50
50
|
|
51
51
|
if TYPE_CHECKING: # pragma: no cover
|
@@ -420,7 +420,7 @@ class NWProject:
|
|
420
420
|
timeStamp = formatTimeStamp(time(), fileSafe=True)
|
421
421
|
archName = baseDir / f"{cleanName} {timeStamp}.zip"
|
422
422
|
if self._storage.zipIt(archName, compression=2):
|
423
|
-
size = formatInt(archName
|
423
|
+
size = formatInt(getFileSize(archName))
|
424
424
|
if doNotify:
|
425
425
|
SHARED.info(
|
426
426
|
self.tr("Created a backup of your project of size {0}B.").format(size),
|
novelwriter/core/projectdata.py
CHANGED
novelwriter/core/tohtml.py
CHANGED
@@ -287,7 +287,10 @@ class ToHtml(Tokenizer):
|
|
287
287
|
para.append(stripEscape(tTemp.rstrip()))
|
288
288
|
|
289
289
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
290
|
-
lines.append(self._formatSynopsis(tText))
|
290
|
+
lines.append(self._formatSynopsis(tText, True))
|
291
|
+
|
292
|
+
elif tType == self.T_SHORT and self._doSynopsis:
|
293
|
+
lines.append(self._formatSynopsis(tText, False))
|
291
294
|
|
292
295
|
elif tType == self.T_COMMENT and self._doComments:
|
293
296
|
lines.append(self._formatComments(tText))
|
@@ -454,9 +457,12 @@ class ToHtml(Tokenizer):
|
|
454
457
|
# Internal Functions
|
455
458
|
##
|
456
459
|
|
457
|
-
def _formatSynopsis(self, text: str) -> str:
|
460
|
+
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
|
458
461
|
"""Apply HTML formatting to synopsis."""
|
459
|
-
|
462
|
+
if synopsis:
|
463
|
+
sSynop = self._localLookup("Synopsis")
|
464
|
+
else:
|
465
|
+
sSynop = self._localLookup("Short Description")
|
460
466
|
if self._genMode == self.M_PREVIEW:
|
461
467
|
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
|
462
468
|
else:
|
novelwriter/core/tokenizer.py
CHANGED
@@ -34,8 +34,9 @@ from pathlib import Path
|
|
34
34
|
from functools import partial
|
35
35
|
|
36
36
|
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
37
|
+
from novelwriter.core.index import processComment
|
37
38
|
|
38
|
-
from novelwriter.enum import nwItemLayout
|
39
|
+
from novelwriter.enum import nwComment, nwItemLayout
|
39
40
|
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
|
40
41
|
from novelwriter.constants import nwHeadFmt, nwRegEx, nwShortcode, nwUnicode
|
41
42
|
from novelwriter.core.project import NWProject
|
@@ -79,17 +80,18 @@ class Tokenizer(ABC):
|
|
79
80
|
# Block Type
|
80
81
|
T_EMPTY = 1 # Empty line (new paragraph)
|
81
82
|
T_SYNOPSIS = 2 # Synopsis comment
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
83
|
+
T_SHORT = 3 # Short description comment
|
84
|
+
T_COMMENT = 4 # Comment line
|
85
|
+
T_KEYWORD = 5 # Command line
|
86
|
+
T_TITLE = 6 # Title
|
87
|
+
T_UNNUM = 7 # Unnumbered
|
88
|
+
T_HEAD1 = 8 # Header 1
|
89
|
+
T_HEAD2 = 9 # Header 2
|
90
|
+
T_HEAD3 = 10 # Header 3
|
91
|
+
T_HEAD4 = 11 # Header 4
|
92
|
+
T_TEXT = 12 # Text line
|
93
|
+
T_SEP = 13 # Scene separator
|
94
|
+
T_SKIP = 14 # Paragraph break
|
93
95
|
|
94
96
|
# Block Style
|
95
97
|
A_NONE = 0x0000 # No special style
|
@@ -216,7 +218,7 @@ class Tokenizer(ABC):
|
|
216
218
|
return
|
217
219
|
|
218
220
|
def setChapterFormat(self, hFormat: str) -> None:
|
219
|
-
"""Set the
|
221
|
+
"""Set the chapter format pattern."""
|
220
222
|
self._fmtChapter = hFormat.strip()
|
221
223
|
return
|
222
224
|
|
@@ -435,8 +437,8 @@ class Tokenizer(ABC):
|
|
435
437
|
if aLine[0] == "[":
|
436
438
|
# Parse special formatting line
|
437
439
|
# This must be a separate if statement, as it may not
|
438
|
-
# reach a continue statement and must
|
439
|
-
# check other formats.
|
440
|
+
# reach a continue statement and must therefore proceed
|
441
|
+
# to check other formats.
|
440
442
|
|
441
443
|
if sLine in ("[newpage]", "[new page]"):
|
442
444
|
breakNext = True
|
@@ -461,17 +463,22 @@ class Tokenizer(ABC):
|
|
461
463
|
continue
|
462
464
|
|
463
465
|
if aLine[0] == "%":
|
464
|
-
|
465
|
-
|
466
|
-
if synTag == "synopsis:":
|
466
|
+
cStyle, cText, _ = processComment(aLine)
|
467
|
+
if cStyle == nwComment.SYNOPSIS:
|
467
468
|
self._tokens.append((
|
468
|
-
self.T_SYNOPSIS, nHead,
|
469
|
+
self.T_SYNOPSIS, nHead, cText, None, sAlign
|
470
|
+
))
|
471
|
+
if self._doSynopsis and self._keepMarkdown:
|
472
|
+
tmpMarkdown.append("%s\n" % aLine)
|
473
|
+
elif cStyle == nwComment.SHORT:
|
474
|
+
self._tokens.append((
|
475
|
+
self.T_SHORT, nHead, cText, None, sAlign
|
469
476
|
))
|
470
477
|
if self._doSynopsis and self._keepMarkdown:
|
471
478
|
tmpMarkdown.append("%s\n" % aLine)
|
472
479
|
else:
|
473
480
|
self._tokens.append((
|
474
|
-
self.T_COMMENT, nHead,
|
481
|
+
self.T_COMMENT, nHead, cText, None, sAlign
|
475
482
|
))
|
476
483
|
if self._doComments and self._keepMarkdown:
|
477
484
|
tmpMarkdown.append("%s\n" % aLine)
|
novelwriter/core/tomd.py
CHANGED
@@ -170,6 +170,10 @@ class ToMarkdown(Tokenizer):
|
|
170
170
|
label = self._localLookup("Synopsis")
|
171
171
|
lines.append(f"**{label}:** {tText}\n\n")
|
172
172
|
|
173
|
+
elif tType == self.T_SHORT and self._doSynopsis:
|
174
|
+
label = self._localLookup("Short Description")
|
175
|
+
lines.append(f"**{label}:** {tText}\n\n")
|
176
|
+
|
173
177
|
elif tType == self.T_COMMENT and self._doComments:
|
174
178
|
label = self._localLookup("Comment")
|
175
179
|
lines.append(f"**{label}:** {tText}\n\n")
|
novelwriter/core/toodt.py
CHANGED
@@ -195,7 +195,7 @@ class ToOdt(Tokenizer):
|
|
195
195
|
# Setters
|
196
196
|
##
|
197
197
|
|
198
|
-
def setLanguage(self, language: str) -> None:
|
198
|
+
def setLanguage(self, language: str | None) -> None:
|
199
199
|
"""Set language for the document."""
|
200
200
|
if language:
|
201
201
|
langBits = language.split("_")
|
@@ -481,7 +481,11 @@ class ToOdt(Tokenizer):
|
|
481
481
|
pFmt.append(tFormat)
|
482
482
|
|
483
483
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
484
|
-
tTemp, fTemp = self._formatSynopsis(tText)
|
484
|
+
tTemp, fTemp = self._formatSynopsis(tText, True)
|
485
|
+
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
|
486
|
+
|
487
|
+
elif tType == self.T_SHORT and self._doSynopsis:
|
488
|
+
tTemp, fTemp = self._formatSynopsis(tText, False)
|
485
489
|
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
|
486
490
|
|
487
491
|
elif tType == self.T_COMMENT and self._doComments:
|
@@ -552,9 +556,12 @@ class ToOdt(Tokenizer):
|
|
552
556
|
# Internal Functions
|
553
557
|
##
|
554
558
|
|
555
|
-
def _formatSynopsis(self, text: str) -> tuple[str, list[tuple[int, int]]]:
|
559
|
+
def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
|
556
560
|
"""Apply formatting to synopsis lines."""
|
557
|
-
|
561
|
+
if synopsis:
|
562
|
+
name = self._localLookup("Synopsis")
|
563
|
+
else:
|
564
|
+
name = self._localLookup("Short Description")
|
558
565
|
rTxt = f"{name}: {text}"
|
559
566
|
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
560
567
|
return rTxt, rFmt
|