novelWriter 2.3.1__py3-none-any.whl → 2.4b1__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 (81) hide show
  1. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +81 -70
  3. novelwriter/__init__.py +5 -5
  4. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  5. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  6. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  9. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  10. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  11. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  12. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  13. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  14. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  17. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  18. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  19. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +25 -23
  36. novelwriter/common.py +1 -1
  37. novelwriter/config.py +35 -12
  38. novelwriter/constants.py +5 -6
  39. novelwriter/core/buildsettings.py +60 -40
  40. novelwriter/core/coretools.py +98 -13
  41. novelwriter/core/docbuild.py +74 -7
  42. novelwriter/core/document.py +24 -3
  43. novelwriter/core/index.py +31 -112
  44. novelwriter/core/project.py +10 -15
  45. novelwriter/core/sessions.py +2 -2
  46. novelwriter/core/status.py +4 -4
  47. novelwriter/core/storage.py +8 -2
  48. novelwriter/core/tohtml.py +22 -25
  49. novelwriter/core/tokenizer.py +416 -232
  50. novelwriter/core/tomd.py +17 -8
  51. novelwriter/core/toodt.py +65 -7
  52. novelwriter/core/tree.py +8 -8
  53. novelwriter/dialogs/docsplit.py +7 -8
  54. novelwriter/dialogs/preferences.py +3 -6
  55. novelwriter/enum.py +17 -14
  56. novelwriter/extensions/modified.py +20 -2
  57. novelwriter/extensions/versioninfo.py +1 -1
  58. novelwriter/gui/doceditor.py +257 -279
  59. novelwriter/gui/dochighlight.py +29 -25
  60. novelwriter/gui/docviewer.py +139 -148
  61. novelwriter/gui/docviewerpanel.py +4 -24
  62. novelwriter/gui/editordocument.py +12 -1
  63. novelwriter/gui/itemdetails.py +6 -6
  64. novelwriter/gui/mainmenu.py +37 -16
  65. novelwriter/gui/noveltree.py +11 -19
  66. novelwriter/gui/outline.py +43 -20
  67. novelwriter/gui/projtree.py +35 -43
  68. novelwriter/gui/search.py +316 -0
  69. novelwriter/gui/sidebar.py +25 -30
  70. novelwriter/gui/theme.py +59 -6
  71. novelwriter/guimain.py +176 -173
  72. novelwriter/shared.py +26 -1
  73. novelwriter/text/__init__.py +3 -0
  74. novelwriter/text/counting.py +137 -0
  75. novelwriter/tools/manuscript.py +344 -55
  76. novelwriter/tools/manussettings.py +213 -71
  77. novelwriter/tools/welcome.py +1 -1
  78. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
  79. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +0 -0
  80. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/entry_points.txt +0 -0
  81. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
@@ -45,3 +45,4 @@ spellcheckline = 255, 157, 164
45
45
  errorline = 209, 241, 169
46
46
  replacetag = 153, 255, 255
47
47
  modifier = 255, 197, 143
48
+ texthighlight = 255, 238, 173, 96
@@ -45,3 +45,4 @@ spellcheckline = 213, 78, 83
45
45
  errorline = 185, 202, 74
46
46
  replacetag = 112, 192, 177
47
47
  modifier = 231, 140, 69
48
+ texthighlight = 231, 197, 71, 128
@@ -45,3 +45,4 @@ spellcheckline = 242, 119, 122
45
45
  errorline = 153, 204, 153
46
46
  replacetag = 102, 204, 204
47
47
  modifier = 249, 145, 57
48
+ texthighlight = 255, 204, 102, 96
@@ -8,9 +8,9 @@
8
8
  <h3>Contributors</h3>
9
9
 
10
10
  <p>
11
- <b>Concept:</b> Marian Lückhof (<a href="https://github.com/Number042">@Number042</a>)<br>
12
- <b>Internationalisation:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
13
- <b>Setup and Packaging:</b> Rachel Powers (<a href="https://github.com/Ryex">@Ryex</a>)
11
+ &nbsp;&bull;&nbsp;<b>Concept:</b> Marian Lückhof (<a href="https://github.com/Number042">@Number042</a>)<br>
12
+ &nbsp;&bull;&nbsp;<b>Internationalisation:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
13
+ &nbsp;&bull;&nbsp;<b>Setup and Packaging:</b> Rachel Powers (<a href="https://github.com/Ryex">@Ryex</a>)
14
14
  </p>
15
15
  <p>For other contributions, see the project's
16
16
  <a href="https://github.com/vkbo/novelWriter/graphs/contributors">Contributors</a> page.</p>
@@ -24,20 +24,20 @@
24
24
  <p>The default language is English (UK) with English (US) as an option. These are the original
25
25
  translators for the languages currently available:</p>
26
26
  <p>
27
- <b>Dutch:</b> Martijn van der Kleijn (<a href="https://github.com/mvdkleijn">@mvdkleijn</a>)<br>
28
- <b>French:</b> Jan Lüdke (<a href="https://github.com/jyhelle">@jyhelle</a>)<br>
29
- <b>German:</b> Myian (<a href="https://github.com/heymyian">@heymyian</a>)<br>
30
- <b>Italian:</b> Riccardo Mangili<br>
31
- <b>Japanese:</b> hebekeg (<a href="https://github.com/hebekeg">@hebekeg</a>)<br>
32
- <b>Latin American Spanish:</b> Tommy Marplatt (<a href="https://github.com/tmarplatt">@tmarplatt</a>)<br>
33
- <b>Norwegian:</b> Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)<br>
34
- <b>Portuguese:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
35
- <b>Simplified Chinese:</b> Qianzhi Long (<a href="https://github.com/longqzh">@longqzh</a>)
27
+ &nbsp;&bull;&nbsp;<b>Dutch:</b> Martijn van der Kleijn (<a href="https://github.com/mvdkleijn">@mvdkleijn</a>)<br>
28
+ &nbsp;&bull;&nbsp;<b>French:</b> Jan Lüdke (<a href="https://github.com/jyhelle">@jyhelle</a>)<br>
29
+ &nbsp;&bull;&nbsp;<b>German:</b> Myian (<a href="https://github.com/heymyian">@heymyian</a>)<br>
30
+ &nbsp;&bull;&nbsp;<b>Italian:</b> Riccardo Mangili<br>
31
+ &nbsp;&bull;&nbsp;<b>Japanese:</b> hebekeg (<a href="https://github.com/hebekeg">@hebekeg</a>)<br>
32
+ &nbsp;&bull;&nbsp;<b>Latin American Spanish:</b> Tommy Marplatt (<a href="https://github.com/tmarplatt">@tmarplatt</a>)<br>
33
+ &nbsp;&bull;&nbsp;<b>Norwegian:</b> Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)<br>
34
+ &nbsp;&bull;&nbsp;<b>Portuguese:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
35
+ &nbsp;&bull;&nbsp;<b>Simplified Chinese:</b> Qianzhi Long (<a href="https://github.com/longqzh">@longqzh</a>)
36
36
  </p>
37
37
 
38
38
  <p>Additional larger translation contributions:</p>
39
39
  <p>
40
- <b>French:</b> Albert Aribaud (<a href="https://github.com/aaribaud">@aaribaud</a>)
40
+ &nbsp;&bull;&nbsp;<b>French:</b> Albert Aribaud (<a href="https://github.com/aaribaud">@aaribaud</a>)
41
41
  </p>
42
42
 
43
43
  <p>Translations are managed on <a href="https://crowdin.com/project/novelwriter">Crowdin</a>, and
@@ -47,27 +47,29 @@ more contributions are listed on the project's <a href="https://crowdin.com/proj
47
47
 
48
48
  <p>The following libraries are dependencies of novelWriter:</p>
49
49
  <p>
50
- <a href="https://www.qt.io">Qt5</a> by Qt Company<br>
51
- <a href="https://www.riverbankcomputing.com/software/pyqt">PyQt5</a> by Riverbank Computing<br>
52
- <a href="https://abiword.github.io/enchant">Enchant</a> by Dom Lachowicz<br>
53
- <a href="https://pyenchant.github.io/pyenchant">PyEnchant</a> by Dimitri Merejkowsky
50
+ &nbsp;&bull;&nbsp;<a href="https://www.qt.io">Qt5</a> by Qt Company<br>
51
+ &nbsp;&bull;&nbsp;<a href="https://www.riverbankcomputing.com/software/pyqt">PyQt5</a> by Riverbank Computing<br>
52
+ &nbsp;&bull;&nbsp;<a href="https://abiword.github.io/enchant">Enchant</a> by Dom Lachowicz<br>
53
+ &nbsp;&bull;&nbsp;<a href="https://pyenchant.github.io/pyenchant">PyEnchant</a> by Dimitri Merejkowsky
54
54
  </p>
55
55
 
56
56
  <h3>Assets</h3>
57
57
 
58
58
  <p>Some of the assets bundled with novelWriter were adapted from the following sources:</p>
59
59
  <p>
60
- <a href="https://github.com/stephenhutchings/typicons.font">Typicons</a> icons by Stephen Hutchings (CC BY-SA 4.0)<br>
61
- <a href="https://github.com/chriskempson/base16">Tomorrow</a> syntax themes by Chris Kempson (MIT License)<br>
62
- <a href="https://github.com/sdras/night-owl-vscode-theme">Owl</a> syntax themes by Sarah Drasner (MIT License)<br>
63
- <a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)
60
+ &nbsp;&bull;&nbsp;<a href="https://github.com/stephenhutchings/typicons.font">Typicons</a> icons by Stephen Hutchings (CC BY-SA 4.0)<br>
61
+ &nbsp;&bull;&nbsp;<a href="https://github.com/chriskempson/base16">Tomorrow</a> syntax themes by Chris Kempson (MIT License)<br>
62
+ &nbsp;&bull;&nbsp;<a href="https://github.com/sdras/night-owl-vscode-theme">Owl</a> syntax themes by Sarah Drasner (MIT License)<br>
63
+ &nbsp;&bull;&nbsp;<a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)
64
64
  </p>
65
65
 
66
66
  <h3>Fonts</h3>
67
67
 
68
- <p>The font used for the main novelWriter logo, mimetype and text banners is:</p>
68
+ <p>The font used for the main novelWriter logo, mimetype and text banners is Pridi. Other fonts are
69
+ used on buttons and icons.</p>
69
70
  <p>
70
- Pridi by Cadson Demak (Open Font License, Version 1.1)
71
+ &nbsp;&bull;&nbsp;Pridi by Cadson Demak (Open Font License, Version 1.1)<br>
72
+ &nbsp;&bull;&nbsp;Source Sans Pro by Paul D. Hunt (SIL Open Font License)
71
73
  </p>
72
74
 
73
75
  <h3>Special Mentions</h3>
novelwriter/common.py CHANGED
@@ -255,7 +255,7 @@ def formatFileFilter(extensions: list[str | tuple[str, str]]) -> str:
255
255
  result = []
256
256
  for ext in extensions:
257
257
  if isinstance(ext, str):
258
- result.append(f"{trConst(nwLabels.FILE_FILTERS.get(ext))} ({ext})")
258
+ result.append(f"{trConst(nwLabels.FILE_FILTERS.get(ext, 'ERR'))} ({ext})")
259
259
  elif isinstance(ext, tuple) and len(ext) == 2:
260
260
  result.append(f"{ext[0]} ({ext[1]})")
261
261
  return ";;".join(result)
novelwriter/config.py CHANGED
@@ -30,6 +30,7 @@ import logging
30
30
 
31
31
  from time import time
32
32
  from pathlib import Path
33
+ from datetime import datetime
33
34
 
34
35
  from PyQt5.QtGui import QFontDatabase
35
36
  from PyQt5.QtCore import (
@@ -84,8 +85,11 @@ class Config:
84
85
  self._nwLangPath = self._appPath / "assets" / "i18n"
85
86
  self._qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
86
87
 
87
- wantedLocale = self._nwLangPath / f"nw_{QLocale.system().name()}.qm"
88
- self._qLocale = QLocale.system() if wantedLocale.exists() else QLocale("en_GB")
88
+ hasLocale = (self._nwLangPath / f"nw_{QLocale.system().name()}.qm").exists()
89
+ self._qLocale = QLocale.system() if hasLocale else QLocale("en_GB")
90
+ self._dLocale = QLocale.system()
91
+ self._dShortDate = self._dLocale.dateFormat(QLocale.FormatType.ShortFormat)
92
+ self._dShortDateTime = self._dLocale.dateTimeFormat(QLocale.FormatType.ShortFormat)
89
93
  self._qtTrans = {}
90
94
 
91
95
  # PDF Manual
@@ -179,17 +183,19 @@ class Config:
179
183
  # State
180
184
  self.showViewerPanel = True # The panel for the viewer is visible
181
185
  self.showEditToolBar = False # The document editor toolbar visibility
182
- self.useShortcodes = False # Use shortcodes for basic formatting
183
186
  self.viewComments = True # Comments are shown in the viewer
184
187
  self.viewSynopsis = True # Synopsis is shown in the viewer
185
188
 
186
- # Search Bar Switches
187
- self.searchCase = False
188
- self.searchWord = False
189
- self.searchRegEx = False
190
- self.searchLoop = False
191
- self.searchNextFile = False
192
- self.searchMatchCap = False
189
+ # Search Box States
190
+ self.searchCase = False
191
+ self.searchWord = False
192
+ self.searchRegEx = False
193
+ self.searchLoop = False
194
+ self.searchNextFile = False
195
+ self.searchMatchCap = False
196
+ self.searchProjCase = False
197
+ self.searchProjWord = False
198
+ self.searchProjRegEx = False
193
199
 
194
200
  # System and App Information
195
201
  # ==========================
@@ -414,6 +420,14 @@ class Config:
414
420
  self._errData = []
415
421
  return message
416
422
 
423
+ def localDate(self, value: datetime) -> str:
424
+ """Return a localised date format."""
425
+ return self._dLocale.toString(value, self._dShortDate)
426
+
427
+ def localDateTime(self, value: datetime) -> str:
428
+ """Return a localised datetime format."""
429
+ return self._dLocale.toString(value, self._dShortDateTime)
430
+
417
431
  def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
418
432
  """List localisation files in the i18n folder. The default GUI
419
433
  language is British English (en_GB).
@@ -496,6 +510,11 @@ class Config:
496
510
  QLocale.setDefault(self._qLocale)
497
511
  self._qtTrans = {}
498
512
 
513
+ hasLocale = (self._nwLangPath / f"nw_{self._qLocale.name()}.qm").exists()
514
+ self._dLocale = self._qLocale if hasLocale else QLocale.system()
515
+ self._dShortDate = self._dLocale.dateFormat(QLocale.FormatType.ShortFormat)
516
+ self._dShortDateTime = self._dLocale.dateTimeFormat(QLocale.FormatType.ShortFormat)
517
+
499
518
  langList = [
500
519
  (self._qtLangPath, "qtbase"), # Qt 5.x
501
520
  (str(self._nwLangPath), "nw"), # novelWriter
@@ -602,7 +621,6 @@ class Config:
602
621
  sec = "State"
603
622
  self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
604
623
  self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
605
- self.useShortcodes = conf.rdBool(sec, "useshortcodes", self.useShortcodes)
606
624
  self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
607
625
  self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis)
608
626
  self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase)
@@ -611,6 +629,9 @@ class Config:
611
629
  self.searchLoop = conf.rdBool(sec, "searchloop", self.searchLoop)
612
630
  self.searchNextFile = conf.rdBool(sec, "searchnextfile", self.searchNextFile)
613
631
  self.searchMatchCap = conf.rdBool(sec, "searchmatchcap", self.searchMatchCap)
632
+ self.searchProjCase = conf.rdBool(sec, "searchprojcase", self.searchProjCase)
633
+ self.searchProjWord = conf.rdBool(sec, "searchprojword", self.searchProjWord)
634
+ self.searchProjRegEx = conf.rdBool(sec, "searchprojregex", self.searchProjRegEx)
614
635
 
615
636
  # Check Values
616
637
  # ============
@@ -708,7 +729,6 @@ class Config:
708
729
  conf["State"] = {
709
730
  "showviewerpanel": str(self.showViewerPanel),
710
731
  "showedittoolbar": str(self.showEditToolBar),
711
- "useshortcodes": str(self.useShortcodes),
712
732
  "viewcomments": str(self.viewComments),
713
733
  "viewsynopsis": str(self.viewSynopsis),
714
734
  "searchcase": str(self.searchCase),
@@ -717,6 +737,9 @@ class Config:
717
737
  "searchloop": str(self.searchLoop),
718
738
  "searchnextfile": str(self.searchNextFile),
719
739
  "searchmatchcap": str(self.searchMatchCap),
740
+ "searchprojcase": str(self.searchProjCase),
741
+ "searchprojword": str(self.searchProjWord),
742
+ "searchprojregex": str(self.searchProjRegEx),
720
743
  }
721
744
 
722
745
  # Write config file
novelwriter/constants.py CHANGED
@@ -23,8 +23,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
- import re
27
-
28
26
  from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
29
27
 
30
28
  from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
@@ -55,6 +53,7 @@ class nwConst:
55
53
 
56
54
  # Gui Settings
57
55
  STATUS_MSG_TIMEOUT = 15000 # milliseconds
56
+ MAX_SEARCH_RESULT = 1000
58
57
 
59
58
  # Dialogs
60
59
  DLG_FINISHED = 2
@@ -67,12 +66,9 @@ class nwRegEx:
67
66
  FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
68
67
  FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
69
68
  FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
70
- FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|sup|sub)\])"
69
+ FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|m|sup|sub)\])"
71
70
  FMT_SV = r"(?<!\\)(\[(?i)(?:fn|footnote):)(.+?)(?<!\\)(\])"
72
71
 
73
- # Pre-Compiled RegEx
74
- RX_SC = re.compile(FMT_SC)
75
-
76
72
  # END Class nwRegEx
77
73
 
78
74
 
@@ -86,6 +82,8 @@ class nwShortcode:
86
82
  STRIKE_C = "[/s]"
87
83
  ULINE_O = "[u]"
88
84
  ULINE_C = "[/u]"
85
+ MARK_O = "[m]"
86
+ MARK_C = "[/m]"
89
87
  SUP_O = "[sup]"
90
88
  SUP_C = "[/sup]"
91
89
  SUB_O = "[sub]"
@@ -408,6 +406,7 @@ class nwUnicode:
408
406
  U_THNBSP = "\u202f" # Thin non-breaking space
409
407
  U_ENSP = "\u2002" # Short (en) space
410
408
  U_EMSP = "\u2003" # Long (em) space
409
+ U_MMSP = "\u205f" # Medium mathematical space
411
410
  U_LSEP = "\u2028" # Line separator
412
411
  U_PSEP = "\u2029" # Paragraph separator
413
412
 
@@ -49,39 +49,54 @@ logger = logging.getLogger(__name__)
49
49
  # (type, default, [min value, max value])
50
50
 
51
51
  SETTINGS_TEMPLATE = {
52
- "filter.includeNovel": (bool, True),
53
- "filter.includeNotes": (bool, False),
54
- "filter.includeInactive": (bool, False),
55
- "headings.fmtTitle": (str, nwHeadFmt.TITLE),
56
- "headings.fmtChapter": (str, nwHeadFmt.TITLE),
57
- "headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
58
- "headings.fmtScene": (str, "* * *"),
59
- "headings.fmtSection": (str, ""),
60
- "headings.hideScene": (bool, False),
61
- "headings.hideSection": (bool, True),
62
- "text.includeSynopsis": (bool, False),
63
- "text.includeComments": (bool, False),
64
- "text.includeKeywords": (bool, False),
65
- "text.includeBodyText": (bool, True),
66
- "text.addNoteHeadings": (bool, True),
67
- "format.textFont": (str, CONFIG.textFont),
68
- "format.textSize": (int, 12),
69
- "format.lineHeight": (float, 1.15, 0.75, 3.0),
70
- "format.justifyText": (bool, False),
71
- "format.stripUnicode": (bool, False),
72
- "format.replaceTabs": (bool, False),
73
- "format.pageUnit": (str, "cm"),
74
- "format.pageSize": (str, "A4"),
75
- "format.pageWidth": (float, 21.0),
76
- "format.pageHeight": (float, 29.7),
77
- "format.topMargin": (float, 2.0),
78
- "format.bottomMargin": (float, 2.0),
79
- "format.leftMargin": (float, 2.0),
80
- "format.rightMargin": (float, 2.0),
81
- "odt.addColours": (bool, True),
82
- "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
83
- "odt.pageCountOffset": (int, 0),
84
- "html.addStyles": (bool, True),
52
+ "filter.includeNovel": (bool, True),
53
+ "filter.includeNotes": (bool, False),
54
+ "filter.includeInactive": (bool, False),
55
+ "headings.fmtTitle": (str, nwHeadFmt.TITLE),
56
+ "headings.fmtChapter": (str, nwHeadFmt.TITLE),
57
+ "headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
58
+ "headings.fmtScene": (str, "* * *"),
59
+ "headings.fmtHardScene": (str, ""),
60
+ "headings.fmtSection": (str, ""),
61
+ "headings.hideTitle": (bool, False),
62
+ "headings.hideChapter": (bool, False),
63
+ "headings.hideUnnumbered": (bool, False),
64
+ "headings.hideScene": (bool, False),
65
+ "headings.hideHardScene": (bool, False),
66
+ "headings.hideSection": (bool, True),
67
+ "headings.centerTitle": (bool, True),
68
+ "headings.centerChapter": (bool, False),
69
+ "headings.centerScene": (bool, False),
70
+ "headings.breakTitle": (bool, True),
71
+ "headings.breakChapter": (bool, True),
72
+ "headings.breakScene": (bool, False),
73
+ "text.includeSynopsis": (bool, False),
74
+ "text.includeComments": (bool, False),
75
+ "text.includeKeywords": (bool, False),
76
+ "text.includeBodyText": (bool, True),
77
+ "text.ignoredKeywords": (str, ""),
78
+ "text.addNoteHeadings": (bool, True),
79
+ "format.textFont": (str, CONFIG.textFont),
80
+ "format.textSize": (int, 12),
81
+ "format.lineHeight": (float, 1.15, 0.75, 3.0),
82
+ "format.justifyText": (bool, False),
83
+ "format.stripUnicode": (bool, False),
84
+ "format.replaceTabs": (bool, False),
85
+ "format.pageUnit": (str, "cm"),
86
+ "format.pageSize": (str, "A4"),
87
+ "format.pageWidth": (float, 21.0),
88
+ "format.pageHeight": (float, 29.7),
89
+ "format.topMargin": (float, 2.0),
90
+ "format.bottomMargin": (float, 2.0),
91
+ "format.leftMargin": (float, 2.0),
92
+ "format.rightMargin": (float, 2.0),
93
+ "odt.addColours": (bool, True),
94
+ "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
95
+ "odt.pageCountOffset": (int, 0),
96
+ "odt.firstLineIndent": (bool, False),
97
+ "md.preserveBreaks": (bool, True),
98
+ "html.addStyles": (bool, True),
99
+ "html.preserveTabs": (bool, False),
85
100
  }
86
101
 
87
102
  SETTINGS_LABELS = {
@@ -91,19 +106,19 @@ SETTINGS_LABELS = {
91
106
  "filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
92
107
 
93
108
  "headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
94
- "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Title Headings"),
95
- "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Headings"),
96
- "headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Headings"),
97
- "headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Headings"),
98
- "headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Headings"),
99
- "headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene Headings"),
100
- "headings.hideSection": QT_TRANSLATE_NOOP("Builds", "Hide Section Headings"),
109
+ "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
110
+ "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
111
+ "headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
112
+ "headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
113
+ "headings.fmtHardScene": QT_TRANSLATE_NOOP("Builds", "Hard Scene Format"),
114
+ "headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
101
115
 
102
116
  "text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
103
117
  "text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
104
118
  "text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
105
119
  "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
106
120
  "text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
121
+ "text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
107
122
  "text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
108
123
  "text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
109
124
 
@@ -129,9 +144,14 @@ SETTINGS_LABELS = {
129
144
  "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
130
145
  "odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
131
146
  "odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
147
+ "odt.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
148
+
149
+ "md": QT_TRANSLATE_NOOP("Builds", "Markdown (.md)"),
150
+ "md.preserveBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
132
151
 
133
152
  "html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
134
153
  "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
154
+ "html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
135
155
  }
136
156
 
137
157
 
@@ -26,19 +26,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
26
26
  """
27
27
  from __future__ import annotations
28
28
 
29
- import shutil
30
29
  import logging
30
+ import shutil
31
31
 
32
- from pathlib import Path
32
+ from collections.abc import Iterable
33
33
  from functools import partial
34
+ from pathlib import Path
34
35
  from zipfile import ZipFile, is_zipfile
35
- from collections.abc import Iterable
36
36
 
37
- from PyQt5.QtCore import QCoreApplication
37
+ from PyQt5.QtCore import QCoreApplication, QRegularExpression
38
38
 
39
39
  from novelwriter import CONFIG, SHARED
40
40
  from novelwriter.common import isHandle, minmax, simplified
41
- from novelwriter.constants import nwFiles, nwItemClass
41
+ from novelwriter.constants import nwConst, nwFiles, nwItemClass
42
42
  from novelwriter.core.item import NWItem
43
43
  from novelwriter.core.project import NWProject
44
44
  from novelwriter.core.storage import NWStorageCreate
@@ -101,9 +101,7 @@ class DocMerger:
101
101
  if srcItem is None:
102
102
  return False
103
103
 
104
- inDoc = self._project.storage.getDocument(srcHandle)
105
- docText = (inDoc.readDocument() or "").rstrip("\n")
106
-
104
+ docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n")
107
105
  if addComment:
108
106
  docInfo = srcItem.describeMe()
109
107
  docSt, _ = srcItem.getImportStatus(incIcon=False)
@@ -122,9 +120,8 @@ class DocMerger:
122
120
  return False
123
121
 
124
122
  outDoc = self._project.storage.getDocument(self._targetDoc)
125
- docText = (outDoc.readDocument() or "").rstrip("\n")
126
- if docText:
127
- self._targetText.insert(0, docText)
123
+ if text := (outDoc.readDocument() or "").rstrip("\n"):
124
+ self._targetText.insert(0, text)
128
125
 
129
126
  status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
130
127
  if not status:
@@ -292,11 +289,10 @@ class DocDuplicator:
292
289
  newItem.setParent(hMap[newItem.itemParent])
293
290
  self._project.tree.updateItemData(newItem.itemHandle)
294
291
  if newItem.isFileType():
295
- oldDoc = self._project.storage.getDocument(tHandle)
296
292
  newDoc = self._project.storage.getDocument(newItem.itemHandle)
297
293
  if newDoc.fileExists():
298
294
  return
299
- newDoc.writeDocument(oldDoc.readDocument() or "")
295
+ newDoc.writeDocument(self._project.storage.getDocumentText(tHandle))
300
296
  yield newItem.itemHandle, nHandle
301
297
  nHandle = None
302
298
  return
@@ -304,6 +300,95 @@ class DocDuplicator:
304
300
  # END Class DocDuplicator
305
301
 
306
302
 
303
+ class DocSearch:
304
+
305
+ def __init__(self) -> None:
306
+ self._regEx = QRegularExpression()
307
+ self.setCaseSensitive(False)
308
+ self._words = False
309
+ self._escape = True
310
+ return
311
+
312
+ ##
313
+ # Methods
314
+ ##
315
+
316
+ def setCaseSensitive(self, state: bool) -> None:
317
+ """Set the case sensitive search flag."""
318
+ opts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
319
+ if not state:
320
+ opts |= QRegularExpression.PatternOption.CaseInsensitiveOption
321
+ self._regEx.setPatternOptions(opts)
322
+ return
323
+
324
+ def setWholeWords(self, state: bool) -> None:
325
+ """Set the whole words search flag."""
326
+ self._words = state
327
+ return
328
+
329
+ def setUserRegEx(self, state: bool) -> None:
330
+ """Set the escape flag to the opposite state."""
331
+ self._escape = not state
332
+ return
333
+
334
+ def iterSearch(
335
+ self, project: NWProject, search: str
336
+ ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
337
+ """Iteratively search through documents in a project."""
338
+ self._regEx.setPattern(self._buildPattern(search))
339
+ logger.debug("Searching with pattern '%s'", self._regEx.pattern())
340
+
341
+ num = len(search)
342
+ storage = project.storage
343
+ for item in project.tree:
344
+ if item.isFileType():
345
+ text = storage.getDocumentText(item.itemHandle)
346
+ rxItt = self._regEx.globalMatch(text)
347
+ count = 0
348
+ capped = False
349
+ results = []
350
+ while rxItt.hasNext():
351
+ rxMatch = rxItt.next()
352
+ pos = rxMatch.capturedStart()
353
+ num = rxMatch.capturedLength()
354
+ context = text[pos:pos+100].partition("\n")[0]
355
+ if context:
356
+ results.append((pos, num, context))
357
+ count += 1
358
+ if count >= nwConst.MAX_SEARCH_RESULT:
359
+ capped = True
360
+ break
361
+
362
+ yield item, results, capped
363
+
364
+ return
365
+
366
+ ##
367
+ # Internal Functions
368
+ ##
369
+
370
+ def _buildPattern(self, search: str) -> str:
371
+ """Build the search pattern string."""
372
+ if self._escape:
373
+ if CONFIG.verQtValue >= 0x050f00:
374
+ search = QRegularExpression.escape(search)
375
+ else:
376
+ # For older Qt versions, we escape manually
377
+ escaped = ""
378
+ for c in search:
379
+ if c.isalnum() or c == "_":
380
+ escaped += c
381
+ else:
382
+ escaped += f"\\{c}"
383
+ search = escaped
384
+ if self._words:
385
+ search = search if search.startswith("\\b") else f"\\b{search}"
386
+ search = search if search.endswith("\\b") else f"{search}\\b"
387
+ return search
388
+
389
+ # END Class DocSearch
390
+
391
+
307
392
  class ProjectBuilder:
308
393
  """A class to build a new project from a set of user-defined
309
394
  parameter provided by the New Project Wizard.