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.
Files changed (62) hide show
  1. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +60 -48
  3. novelwriter/__init__.py +3 -3
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/novelwriter.ico +0 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +8 -1
  7. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  10. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  11. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  12. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  13. novelwriter/assets/icons/typicons_light/icons.conf +8 -1
  14. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  17. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  18. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  19. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  20. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  21. novelwriter/assets/manual.pdf +0 -0
  22. novelwriter/assets/sample.zip +0 -0
  23. novelwriter/assets/text/release_notes.htm +4 -4
  24. novelwriter/common.py +22 -1
  25. novelwriter/config.py +12 -27
  26. novelwriter/constants.py +20 -3
  27. novelwriter/core/buildsettings.py +1 -1
  28. novelwriter/core/coretools.py +6 -1
  29. novelwriter/core/index.py +100 -34
  30. novelwriter/core/options.py +3 -0
  31. novelwriter/core/project.py +2 -2
  32. novelwriter/core/projectdata.py +1 -1
  33. novelwriter/core/tohtml.py +9 -3
  34. novelwriter/core/tokenizer.py +27 -20
  35. novelwriter/core/tomd.py +4 -0
  36. novelwriter/core/toodt.py +11 -4
  37. novelwriter/dialogs/preferences.py +80 -82
  38. novelwriter/dialogs/updates.py +25 -14
  39. novelwriter/enum.py +14 -4
  40. novelwriter/gui/doceditor.py +282 -177
  41. novelwriter/gui/dochighlight.py +7 -9
  42. novelwriter/gui/docviewer.py +142 -319
  43. novelwriter/gui/docviewerpanel.py +457 -0
  44. novelwriter/gui/editordocument.py +1 -1
  45. novelwriter/gui/mainmenu.py +16 -7
  46. novelwriter/gui/outline.py +10 -6
  47. novelwriter/gui/projtree.py +461 -376
  48. novelwriter/gui/sidebar.py +3 -3
  49. novelwriter/gui/statusbar.py +1 -1
  50. novelwriter/gui/theme.py +21 -2
  51. novelwriter/guimain.py +86 -32
  52. novelwriter/shared.py +23 -1
  53. novelwriter/tools/dictionaries.py +268 -0
  54. novelwriter/tools/manusbuild.py +17 -6
  55. novelwriter/tools/manuscript.py +1 -1
  56. novelwriter/tools/writingstats.py +1 -1
  57. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  58. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  59. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  60. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  61. {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  62. {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 decissions
352
+ """Return a dictionary of item handles with filter decisions
353
353
  applied.
354
354
  """
355
355
  result: dict[str, tuple[bool, FilterMode]] = {}
@@ -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(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
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.enum import nwItemClass, nwItemType, nwItemLayout
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
- for tTag in self._itemIndex.allItemTags(tHandle):
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 is None or not self._project.tree.checkType(tHandle, nwItemType.FILE):
142
- return False
143
-
144
- logger.debug("Re-indexing item '%s'", tHandle)
145
- theDoc = self._project.storage.getDocument(tHandle)
146
- self.scanText(tHandle, theDoc.readDocument() or "")
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
- toCheck = line[1:].lstrip()
330
- synTag = toCheck[:9].lower()
331
- tLen = len(line)
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
- if not isActive:
350
- logger.debug("Deleting removed tag '%s'", tTag)
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 list of files referring back to our file, specified
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 getTags(self, itemClass: nwItemClass) -> list[str]:
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
- # Simple Word Counter
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.
@@ -69,6 +69,9 @@ VALID_MAP = {
69
69
  "GuiManuscriptBuild": {
70
70
  "winWidth", "winHeight", "fmtWidth", "sumWidth",
71
71
  },
72
+ "GuiDocViewerPanel": {
73
+ "colWidths",
74
+ }
72
75
  }
73
76
 
74
77
 
@@ -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.stat().st_size)
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),
@@ -170,7 +170,7 @@ class NWProjectData:
170
170
 
171
171
  @property
172
172
  def autoReplace(self) -> dict[str, str]:
173
- """Return the autoreplace dictionary."""
173
+ """Return the auto-replace dictionary."""
174
174
  return self._autoReplace
175
175
 
176
176
  @property
@@ -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
- sSynop = self._localLookup("Synopsis")
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:
@@ -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
- T_COMMENT = 3 # Comment line
83
- T_KEYWORD = 4 # Command line
84
- T_TITLE = 5 # Title
85
- T_UNNUM = 6 # Unnumbered
86
- T_HEAD1 = 7 # Header 1
87
- T_HEAD2 = 8 # Header 2
88
- T_HEAD3 = 9 # Header 3
89
- T_HEAD4 = 10 # Header 4
90
- T_TEXT = 11 # Text line
91
- T_SEP = 12 # Scene separator
92
- T_SKIP = 13 # Paragraph break
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 chapert format pattern."""
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 thefore proceed to
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
- cLine = aLine[1:].lstrip()
465
- synTag = cLine[:9].lower()
466
- if synTag == "synopsis:":
466
+ cStyle, cText, _ = processComment(aLine)
467
+ if cStyle == nwComment.SYNOPSIS:
467
468
  self._tokens.append((
468
- self.T_SYNOPSIS, nHead, cLine[9:].strip(), None, sAlign
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, aLine[1:].strip(), None, sAlign
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
- name = self._localLookup("Synopsis")
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