novelWriter 2.5.1__py3-none-any.whl → 2.6b1__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 (64) hide show
  1. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
  2. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
  3. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  5. novelwriter/assets/i18n/project_en_GB.json +1 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +1 -0
  9. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  10. novelwriter/assets/manual.pdf +0 -0
  11. novelwriter/assets/sample.zip +0 -0
  12. novelwriter/assets/themes/default_light.conf +2 -2
  13. novelwriter/common.py +63 -0
  14. novelwriter/config.py +10 -3
  15. novelwriter/constants.py +153 -60
  16. novelwriter/core/buildsettings.py +66 -39
  17. novelwriter/core/coretools.py +34 -22
  18. novelwriter/core/docbuild.py +130 -169
  19. novelwriter/core/index.py +29 -18
  20. novelwriter/core/item.py +2 -2
  21. novelwriter/core/options.py +4 -1
  22. novelwriter/core/spellcheck.py +9 -14
  23. novelwriter/dialogs/preferences.py +45 -32
  24. novelwriter/dialogs/projectsettings.py +3 -3
  25. novelwriter/enum.py +29 -23
  26. novelwriter/extensions/configlayout.py +24 -11
  27. novelwriter/extensions/modified.py +13 -1
  28. novelwriter/extensions/pagedsidebar.py +5 -5
  29. novelwriter/formats/shared.py +155 -0
  30. novelwriter/formats/todocx.py +1195 -0
  31. novelwriter/formats/tohtml.py +452 -0
  32. novelwriter/{core → formats}/tokenizer.py +483 -485
  33. novelwriter/formats/tomarkdown.py +217 -0
  34. novelwriter/{core → formats}/toodt.py +270 -320
  35. novelwriter/formats/toqdoc.py +436 -0
  36. novelwriter/formats/toraw.py +91 -0
  37. novelwriter/gui/doceditor.py +240 -193
  38. novelwriter/gui/dochighlight.py +96 -84
  39. novelwriter/gui/docviewer.py +56 -30
  40. novelwriter/gui/docviewerpanel.py +3 -3
  41. novelwriter/gui/editordocument.py +17 -2
  42. novelwriter/gui/itemdetails.py +8 -4
  43. novelwriter/gui/mainmenu.py +121 -60
  44. novelwriter/gui/noveltree.py +35 -37
  45. novelwriter/gui/outline.py +186 -238
  46. novelwriter/gui/projtree.py +142 -131
  47. novelwriter/gui/sidebar.py +7 -6
  48. novelwriter/gui/theme.py +5 -4
  49. novelwriter/guimain.py +43 -155
  50. novelwriter/shared.py +14 -4
  51. novelwriter/text/counting.py +2 -0
  52. novelwriter/text/patterns.py +155 -59
  53. novelwriter/tools/manusbuild.py +1 -1
  54. novelwriter/tools/manuscript.py +121 -78
  55. novelwriter/tools/manussettings.py +403 -260
  56. novelwriter/tools/welcome.py +4 -4
  57. novelwriter/tools/writingstats.py +3 -3
  58. novelwriter/types.py +16 -6
  59. novelwriter/core/tohtml.py +0 -530
  60. novelwriter/core/tomarkdown.py +0 -252
  61. novelwriter/core/toqdoc.py +0 -419
  62. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
  63. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
  64. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/top_level.txt +0 -0
novelwriter/guimain.py CHANGED
@@ -44,7 +44,7 @@ from novelwriter.dialogs.about import GuiAbout
44
44
  from novelwriter.dialogs.preferences import GuiPreferences
45
45
  from novelwriter.dialogs.projectsettings import GuiProjectSettings
46
46
  from novelwriter.dialogs.wordlist import GuiWordList
47
- from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView
47
+ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwView
48
48
  from novelwriter.gui.doceditor import GuiDocEditor
49
49
  from novelwriter.gui.docviewer import GuiDocViewer
50
50
  from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
@@ -249,11 +249,13 @@ class GuiMain(QMainWindow):
249
249
  self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
250
250
  self.projSearch.selectedItemChanged.connect(self.itemDetails.updateViewBox)
251
251
 
252
- self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
252
+ self.docEditor.closeEditorRequest.connect(self.closeDocEditor)
253
253
  self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
254
254
  self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
255
255
  self.docEditor.docTextChanged.connect(self.projSearch.textChanged)
256
256
  self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
257
+ self.docEditor.itemHandleChanged.connect(self.novelView.setActiveHandle)
258
+ self.docEditor.itemHandleChanged.connect(self.projView.setActiveHandle)
257
259
  self.docEditor.loadDocumentTagRequest.connect(self._followTag)
258
260
  self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
259
261
  self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
@@ -262,12 +264,13 @@ class GuiMain(QMainWindow):
262
264
  self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
263
265
  self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
264
266
  self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
265
- self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
266
267
  self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
268
+ self.docEditor.updateStatusMessage.connect(self.mainStatus.setStatusMessage)
267
269
 
268
270
  self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
269
271
  self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
270
272
  self.docViewer.loadDocumentTagRequest.connect(self._followTag)
273
+ self.docViewer.openDocumentRequest.connect(self._openDocument)
271
274
  self.docViewer.reloadDocumentRequest.connect(self._reloadViewer)
272
275
  self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
273
276
  self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
@@ -289,19 +292,17 @@ class GuiMain(QMainWindow):
289
292
  self.asDocTimer = QTimer(self)
290
293
  self.asDocTimer.timeout.connect(self._autoSaveDocument)
291
294
 
292
- # Shortcuts and Actions
293
- self._connectMenuActions()
294
-
295
+ # Shortcuts
295
296
  self.keyReturn = QShortcut(self)
296
- self.keyReturn.setKey(Qt.Key.Key_Return)
297
+ self.keyReturn.setKey("Return")
297
298
  self.keyReturn.activated.connect(self._keyPressReturn)
298
299
 
299
300
  self.keyEnter = QShortcut(self)
300
- self.keyEnter.setKey(Qt.Key.Key_Enter)
301
+ self.keyEnter.setKey("Enter")
301
302
  self.keyEnter.activated.connect(self._keyPressReturn)
302
303
 
303
304
  self.keyEscape = QShortcut(self)
304
- self.keyEscape.setKey(Qt.Key.Key_Escape)
305
+ self.keyEscape.setKey("Esc")
305
306
  self.keyEscape.activated.connect(self._keyPressEscape)
306
307
 
307
308
  # Initialise Main GUI
@@ -481,8 +482,7 @@ class GuiMain(QMainWindow):
481
482
  QApplication.processEvents()
482
483
  self.openDocument(lastEdited, doScroll=True)
483
484
 
484
- lastViewed = SHARED.project.data.getLastHandle("viewer")
485
- if lastViewed is not None:
485
+ if lastViewed := SHARED.project.data.getLastHandle("viewer"):
486
486
  QApplication.processEvents()
487
487
  self.viewDocument(lastViewed)
488
488
 
@@ -512,7 +512,7 @@ class GuiMain(QMainWindow):
512
512
  # Document Actions
513
513
  ##
514
514
 
515
- def closeDocument(self, beforeOpen: bool = False) -> None:
515
+ def closeDocument(self) -> None:
516
516
  """Close the document and clear the editor and title field."""
517
517
  if SHARED.hasProject:
518
518
  # Disable focus mode if it is active
@@ -520,8 +520,6 @@ class GuiMain(QMainWindow):
520
520
  SHARED.setFocusMode(False)
521
521
  self.saveDocument()
522
522
  self.docEditor.clearEditor()
523
- if not beforeOpen:
524
- self.novelView.setActiveHandle(None)
525
523
  return
526
524
 
527
525
  def openDocument(
@@ -533,12 +531,8 @@ class GuiMain(QMainWindow):
533
531
  doScroll: bool = False
534
532
  ) -> bool:
535
533
  """Open a specific document, optionally at a given line."""
536
- if not SHARED.hasProject:
537
- logger.error("No project open")
538
- return False
539
-
540
- if not tHandle or not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
541
- logger.debug("Requested item '%s' is not a document", tHandle)
534
+ if not (SHARED.hasProject and tHandle):
535
+ logger.error("Nothing to open open")
542
536
  return False
543
537
 
544
538
  if sTitle and tLine is None:
@@ -548,19 +542,15 @@ class GuiMain(QMainWindow):
548
542
  self._changeView(nwView.EDITOR)
549
543
  if tHandle == self.docEditor.docHandle:
550
544
  self.docEditor.setCursorLine(tLine)
551
- if changeFocus:
552
- self.docEditor.setFocus()
553
- return True
554
-
555
- self.closeDocument(beforeOpen=True)
556
- if self.docEditor.loadText(tHandle, tLine):
557
- SHARED.project.data.setLastHandle(tHandle, "editor")
558
- self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
559
- self.novelView.setActiveHandle(tHandle, doScroll=doScroll)
560
- if changeFocus:
561
- self.docEditor.setFocus()
562
545
  else:
563
- return False
546
+ self.closeDocument()
547
+ if self.docEditor.loadText(tHandle, tLine):
548
+ self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
549
+ else:
550
+ return False
551
+
552
+ if changeFocus:
553
+ self.docEditor.setFocus()
564
554
 
565
555
  return True
566
556
 
@@ -855,13 +845,11 @@ class GuiMain(QMainWindow):
855
845
 
856
846
  def closeMain(self) -> bool:
857
847
  """Save everything, and close novelWriter."""
858
- if SHARED.hasProject:
859
- msgYes = SHARED.question("%s<br>%s" % (
860
- self.tr("Do you want to exit novelWriter?"),
861
- self.tr("Changes are saved automatically.")
862
- ))
863
- if not msgYes:
864
- return False
848
+ if SHARED.hasProject and not SHARED.question("%s<br>%s" % (
849
+ self.tr("Do you want to exit novelWriter?"),
850
+ self.tr("Changes are saved automatically.")
851
+ )):
852
+ return False
865
853
 
866
854
  logger.info("Exiting novelWriter")
867
855
 
@@ -906,11 +894,6 @@ class GuiMain(QMainWindow):
906
894
 
907
895
  return not self.splitView.isVisible()
908
896
 
909
- def toggleFullScreenMode(self) -> None:
910
- """Toggle full screen mode"""
911
- self.setWindowState(self.windowState() ^ Qt.WindowState.WindowFullScreen)
912
- return
913
-
914
897
  ##
915
898
  # Events
916
899
  ##
@@ -926,6 +909,12 @@ class GuiMain(QMainWindow):
926
909
  # Public Slots
927
910
  ##
928
911
 
912
+ @pyqtSlot()
913
+ def toggleFullScreenMode(self) -> None:
914
+ """Toggle full screen mode"""
915
+ self.setWindowState(self.windowState() ^ Qt.WindowState.WindowFullScreen)
916
+ return
917
+
929
918
  @pyqtSlot()
930
919
  def closeDocEditor(self) -> None:
931
920
  """Close the document editor. This does not hide the editor."""
@@ -1111,8 +1100,16 @@ class GuiMain(QMainWindow):
1111
1100
  @pyqtSlot(str, nwDocMode)
1112
1101
  def _followTag(self, tag: str, mode: nwDocMode) -> None:
1113
1102
  """Follow a tag after user interaction with a link."""
1114
- tHandle, sTitle = self._getTagSource(tag)
1115
- if tHandle is not None:
1103
+ tHandle, sTitle = SHARED.project.index.getTagSource(tag)
1104
+ if tHandle is None:
1105
+ SHARED.error(self.tr(
1106
+ "Could not find the reference for tag '{0}'. It either doesn't "
1107
+ "exist, or the index is out of date. The index can be updated "
1108
+ "from the Tools menu, or by pressing {1}."
1109
+ ).format(
1110
+ tag, "F9"
1111
+ ))
1112
+ else:
1116
1113
  if mode == nwDocMode.EDIT:
1117
1114
  self.openDocument(tHandle, sTitle=sTitle)
1118
1115
  elif mode == nwDocMode.VIEW:
@@ -1299,116 +1296,7 @@ class GuiMain(QMainWindow):
1299
1296
  # Internal Functions
1300
1297
  ##
1301
1298
 
1302
- def _connectMenuActions(self) -> None:
1303
- """Connect to the main window all menu actions that need to be
1304
- available also when the main menu is hidden.
1305
- """
1306
- # Project
1307
- self.addAction(self.mainMenu.aSaveProject)
1308
- self.addAction(self.mainMenu.aEditItem)
1309
- self.addAction(self.mainMenu.aExitNW)
1310
-
1311
- # Document
1312
- self.addAction(self.mainMenu.aSaveDoc)
1313
- self.addAction(self.mainMenu.aCloseDoc)
1314
-
1315
- # Edit
1316
- self.addAction(self.mainMenu.aEditUndo)
1317
- self.addAction(self.mainMenu.aEditRedo)
1318
- self.addAction(self.mainMenu.aEditCut)
1319
- self.addAction(self.mainMenu.aEditCopy)
1320
- self.addAction(self.mainMenu.aEditPaste)
1321
- self.addAction(self.mainMenu.aSelectAll)
1322
- self.addAction(self.mainMenu.aSelectPar)
1323
-
1324
- # View
1325
- self.addAction(self.mainMenu.aFocusMode)
1326
- self.addAction(self.mainMenu.aFullScreen)
1327
-
1328
- # Insert
1329
- self.addAction(self.mainMenu.aInsENDash)
1330
- self.addAction(self.mainMenu.aInsEMDash)
1331
- self.addAction(self.mainMenu.aInsHorBar)
1332
- self.addAction(self.mainMenu.aInsFigDash)
1333
- self.addAction(self.mainMenu.aInsQuoteLS)
1334
- self.addAction(self.mainMenu.aInsQuoteRS)
1335
- self.addAction(self.mainMenu.aInsQuoteLD)
1336
- self.addAction(self.mainMenu.aInsQuoteRD)
1337
- self.addAction(self.mainMenu.aInsMSApos)
1338
- self.addAction(self.mainMenu.aInsEllipsis)
1339
- self.addAction(self.mainMenu.aInsPrime)
1340
- self.addAction(self.mainMenu.aInsDPrime)
1341
- self.addAction(self.mainMenu.aInsNBSpace)
1342
- self.addAction(self.mainMenu.aInsThinSpace)
1343
- self.addAction(self.mainMenu.aInsThinNBSpace)
1344
- self.addAction(self.mainMenu.aInsBullet)
1345
- self.addAction(self.mainMenu.aInsHyBull)
1346
- self.addAction(self.mainMenu.aInsFlower)
1347
- self.addAction(self.mainMenu.aInsPerMille)
1348
- self.addAction(self.mainMenu.aInsDegree)
1349
- self.addAction(self.mainMenu.aInsMinus)
1350
- self.addAction(self.mainMenu.aInsTimes)
1351
- self.addAction(self.mainMenu.aInsDivide)
1352
- self.addAction(self.mainMenu.aInsSynopsis)
1353
- self.addAction(self.mainMenu.aInsShort)
1354
-
1355
- for mAction, _ in self.mainMenu.mInsKWItems.values():
1356
- self.addAction(mAction)
1357
-
1358
- # Search
1359
- self.addAction(self.mainMenu.aFind)
1360
- self.addAction(self.mainMenu.aReplace)
1361
- self.addAction(self.mainMenu.aFindNext)
1362
- self.addAction(self.mainMenu.aFindPrev)
1363
- self.addAction(self.mainMenu.aReplaceNext)
1364
-
1365
- # Format
1366
- self.addAction(self.mainMenu.aFmtItalic)
1367
- self.addAction(self.mainMenu.aFmtBold)
1368
- self.addAction(self.mainMenu.aFmtStrike)
1369
- self.addAction(self.mainMenu.aFmtDQuote)
1370
- self.addAction(self.mainMenu.aFmtSQuote)
1371
- self.addAction(self.mainMenu.aFmtHead1)
1372
- self.addAction(self.mainMenu.aFmtHead2)
1373
- self.addAction(self.mainMenu.aFmtHead3)
1374
- self.addAction(self.mainMenu.aFmtHead4)
1375
- self.addAction(self.mainMenu.aFmtAlignLeft)
1376
- self.addAction(self.mainMenu.aFmtAlignCentre)
1377
- self.addAction(self.mainMenu.aFmtAlignRight)
1378
- self.addAction(self.mainMenu.aFmtIndentLeft)
1379
- self.addAction(self.mainMenu.aFmtIndentRight)
1380
- self.addAction(self.mainMenu.aFmtComment)
1381
- self.addAction(self.mainMenu.aFmtNoFormat)
1382
-
1383
- # Tools
1384
- self.addAction(self.mainMenu.aSpellCheck)
1385
- self.addAction(self.mainMenu.aReRunSpell)
1386
- self.addAction(self.mainMenu.aPreferences)
1387
-
1388
- # Help
1389
- self.addAction(self.mainMenu.aHelpDocs)
1390
- if isinstance(CONFIG.pdfDocs, Path):
1391
- self.addAction(self.mainMenu.aPdfDocs)
1392
-
1393
- return
1394
-
1395
1299
  def _updateWindowTitle(self, projName: str | None = None) -> None:
1396
1300
  """Set the window title and add the project's name."""
1397
1301
  self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName])))
1398
1302
  return
1399
-
1400
- def _getTagSource(self, tag: str) -> tuple[str | None, str | None]:
1401
- """Handle the index lookup of a tag and display an alert if the
1402
- tag cannot be found.
1403
- """
1404
- tHandle, sTitle = SHARED.project.index.getTagSource(tag)
1405
- if tHandle is None:
1406
- SHARED.error(self.tr(
1407
- "Could not find the reference for tag '{0}'. It either doesn't "
1408
- "exist, or the index is out of date. The index can be updated "
1409
- "from the Tools menu, or by pressing {1}."
1410
- ).format(
1411
- tag, "F9"
1412
- ))
1413
- return None, None
1414
- return tHandle, sTitle
novelwriter/shared.py CHANGED
@@ -30,8 +30,8 @@ from pathlib import Path
30
30
  from time import time
31
31
  from typing import TYPE_CHECKING, TypeVar
32
32
 
33
- from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
34
- from PyQt5.QtGui import QFont
33
+ from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot
34
+ from PyQt5.QtGui import QDesktopServices, QFont
35
35
  from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
36
36
 
37
37
  from novelwriter.common import formatFileFilter
@@ -292,6 +292,16 @@ class SharedData(QObject):
292
292
  return widget
293
293
  return None
294
294
 
295
+ ##
296
+ # Public Slots
297
+ ##
298
+
299
+ @pyqtSlot(str)
300
+ def openWebsite(self, url: str) -> None:
301
+ """Open a URL in the system's default browser."""
302
+ QDesktopServices.openUrl(QUrl(url))
303
+ return
304
+
295
305
  ##
296
306
  # Signal Proxy
297
307
  ##
@@ -441,9 +451,9 @@ class _GuiAlert(QMessageBox):
441
451
  Yes/No buttons or just an Ok button.
442
452
  """
443
453
  if isYesNo:
444
- self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
454
+ self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
445
455
  else:
446
- self.setStandardButtons(QMessageBox.Ok)
456
+ self.setStandardButtons(QMessageBox.StandardButton.Ok)
447
457
  pSz = 2*self._theme.baseIconHeight
448
458
  if level == self.INFO:
449
459
  self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz)))
@@ -30,6 +30,7 @@ import re
30
30
  from novelwriter.constants import nwRegEx, nwUnicode
31
31
 
32
32
  RX_SC = re.compile(nwRegEx.FMT_SC)
33
+ RX_SV = re.compile(nwRegEx.FMT_SV)
33
34
  RX_LO = re.compile(r"(?i)(?<!\\)(\[(?:vspace|newpage|new page)(:\d+)?)(?<!\\)(\])")
34
35
 
35
36
 
@@ -64,6 +65,7 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
64
65
  # Strip shortcodes and special formatting
65
66
  # RegEx is slow, so we do this only when necessary
66
67
  line = RX_SC.sub("", line)
68
+ line = RX_SV.sub("", line)
67
69
  line = RX_LO.sub("", line)
68
70
 
69
71
  result.append(line)
@@ -3,7 +3,8 @@ novelWriter – Text Pattern Functions
3
3
  ====================================
4
4
 
5
5
  File History:
6
- Created: 2024-06-01 [2.5ec1]
6
+ Created: 2024-06-01 [2.5rc1] RegExPatterns
7
+ Created: 2024-11-04 [2.6b1] DialogParser
7
8
 
8
9
  This file is a part of novelWriter
9
10
  Copyright 2018–2024, Veronica Berglyd Olsen
@@ -23,91 +24,186 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
24
  """
24
25
  from __future__ import annotations
25
26
 
26
- from PyQt5.QtCore import QRegularExpression
27
+ import re
27
28
 
28
29
  from novelwriter import CONFIG
30
+ from novelwriter.common import compact, uniqueCompact
29
31
  from novelwriter.constants import nwRegEx
30
- from novelwriter.types import QRegExUnicode
31
32
 
32
33
 
33
34
  class RegExPatterns:
34
35
 
36
+ # Static RegExes
37
+ _rxUrl = re.compile(nwRegEx.URL, re.ASCII)
38
+ _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE)
39
+ _rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE)
40
+ _rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE)
41
+ _rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE)
42
+ _rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE)
43
+ _rxSCPlain = re.compile(nwRegEx.FMT_SC, re.UNICODE)
44
+ _rxSCValue = re.compile(nwRegEx.FMT_SV, re.UNICODE)
45
+
46
+ @property
47
+ def url(self) -> re.Pattern:
48
+ """Find URLs."""
49
+ return self._rxUrl
50
+
51
+ @property
52
+ def wordSplit(self) -> re.Pattern:
53
+ """Split text into words."""
54
+ return self._rxWords
55
+
35
56
  @property
36
- def markdownItalic(self) -> QRegularExpression:
57
+ def lineBreak(self) -> re.Pattern:
58
+ """Find forced line break."""
59
+ return self._rxBreak
60
+
61
+ @property
62
+ def markdownItalic(self) -> re.Pattern:
37
63
  """Markdown italic style."""
38
- rxRule = QRegularExpression(nwRegEx.FMT_EI)
39
- rxRule.setPatternOptions(QRegExUnicode)
40
- return rxRule
64
+ return self._rxItalic
41
65
 
42
66
  @property
43
- def markdownBold(self) -> QRegularExpression:
67
+ def markdownBold(self) -> re.Pattern:
44
68
  """Markdown bold style."""
45
- rxRule = QRegularExpression(nwRegEx.FMT_EB)
46
- rxRule.setPatternOptions(QRegExUnicode)
47
- return rxRule
69
+ return self._rxBold
48
70
 
49
71
  @property
50
- def markdownStrike(self) -> QRegularExpression:
72
+ def markdownStrike(self) -> re.Pattern:
51
73
  """Markdown strikethrough style."""
52
- rxRule = QRegularExpression(nwRegEx.FMT_ST)
53
- rxRule.setPatternOptions(QRegExUnicode)
54
- return rxRule
74
+ return self._rxStrike
55
75
 
56
76
  @property
57
- def shortcodePlain(self) -> QRegularExpression:
77
+ def shortcodePlain(self) -> re.Pattern:
58
78
  """Plain shortcode style."""
59
- rxRule = QRegularExpression(nwRegEx.FMT_SC)
60
- rxRule.setPatternOptions(QRegExUnicode)
61
- return rxRule
79
+ return self._rxSCPlain
62
80
 
63
81
  @property
64
- def shortcodeValue(self) -> QRegularExpression:
82
+ def shortcodeValue(self) -> re.Pattern:
65
83
  """Plain shortcode style."""
66
- rxRule = QRegularExpression(nwRegEx.FMT_SV)
67
- rxRule.setPatternOptions(QRegExUnicode)
68
- return rxRule
84
+ return self._rxSCValue
69
85
 
70
86
  @property
71
- def dialogStyle(self) -> QRegularExpression:
87
+ def dialogStyle(self) -> re.Pattern | None:
72
88
  """Dialogue detection rule based on user settings."""
73
- symO = ""
74
- symC = ""
75
- if CONFIG.dialogStyle in (1, 3):
76
- symO += CONFIG.fmtSQuoteOpen
77
- symC += CONFIG.fmtSQuoteClose
78
- if CONFIG.dialogStyle in (2, 3):
79
- symO += CONFIG.fmtDQuoteOpen
80
- symC += CONFIG.fmtDQuoteClose
81
-
82
- rxEnd = "|$" if CONFIG.allowOpenDial else ""
83
- rxRule = QRegularExpression(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})")
84
- rxRule.setPatternOptions(QRegExUnicode)
85
- return rxRule
86
-
87
- @property
88
- def dialogLine(self) -> QRegularExpression:
89
- """Dialogue line rule based on user settings."""
90
- sym = QRegularExpression.escape(CONFIG.dialogLine)
91
- rxRule = QRegularExpression(f"^{sym}.*?$")
92
- rxRule.setPatternOptions(QRegExUnicode)
93
- return rxRule
89
+ if CONFIG.dialogStyle > 0:
90
+ end = "|$" if CONFIG.allowOpenDial else ""
91
+ rx = []
92
+ if CONFIG.dialogStyle in (1, 3):
93
+ qO = CONFIG.fmtSQuoteOpen.strip()[:1]
94
+ qC = CONFIG.fmtSQuoteClose.strip()[:1]
95
+ rx.append(f"(?:\\B{qO}.*?(?:{qC}\\B{end}))")
96
+ if CONFIG.dialogStyle in (2, 3):
97
+ qO = CONFIG.fmtDQuoteOpen.strip()[:1]
98
+ qC = CONFIG.fmtDQuoteClose.strip()[:1]
99
+ rx.append(f"(?:\\B{qO}.*?(?:{qC}\\B{end}))")
100
+ return re.compile("|".join(rx), re.UNICODE)
101
+ return None
94
102
 
95
103
  @property
96
- def narratorBreak(self) -> QRegularExpression:
97
- """Dialogue narrator break rule based on user settings."""
98
- sym = QRegularExpression.escape(CONFIG.narratorBreak)
99
- rxRule = QRegularExpression(f"\\B{sym}\\S.*?\\S{sym}\\B")
100
- rxRule.setPatternOptions(QRegExUnicode)
101
- return rxRule
102
-
103
- @property
104
- def altDialogStyle(self) -> QRegularExpression:
104
+ def altDialogStyle(self) -> re.Pattern | None:
105
105
  """Dialogue alternative rule based on user settings."""
106
- symO = QRegularExpression.escape(CONFIG.altDialogOpen)
107
- symC = QRegularExpression.escape(CONFIG.altDialogClose)
108
- rxRule = QRegularExpression(f"\\B{symO}.*?{symC}\\B")
109
- rxRule.setPatternOptions(QRegExUnicode)
110
- return rxRule
106
+ if CONFIG.altDialogOpen and CONFIG.altDialogClose:
107
+ qO = re.escape(compact(CONFIG.altDialogOpen))
108
+ qC = re.escape(compact(CONFIG.altDialogClose))
109
+ return re.compile(f"\\B{qO}.*?{qC}\\B", re.UNICODE)
110
+ return None
111
111
 
112
112
 
113
113
  REGEX_PATTERNS = RegExPatterns()
114
+
115
+
116
+ class DialogParser:
117
+
118
+ __slots__ = (
119
+ "_quotes", "_dialog", "_alternate", "_enabled",
120
+ "_narrator", "_breakD", "_breakQ", "_mode",
121
+ )
122
+
123
+ def __init__(self) -> None:
124
+ self._quotes = None
125
+ self._dialog = ""
126
+ self._alternate = ""
127
+ self._enabled = False
128
+ self._narrator = ""
129
+ self._breakD = None
130
+ self._breakQ = None
131
+ self._mode = ""
132
+ return
133
+
134
+ @property
135
+ def enabled(self) -> bool:
136
+ """Return True if there are any settings to parse."""
137
+ return self._enabled
138
+
139
+ def initParser(self) -> None:
140
+ """Init parser settings. This method must also be called when
141
+ the config changes.
142
+ """
143
+ self._quotes = REGEX_PATTERNS.dialogStyle
144
+ self._dialog = uniqueCompact(CONFIG.dialogLine)
145
+ self._alternate = CONFIG.narratorDialog.strip()[:1]
146
+
147
+ # One of the three modes are needed for the class to have
148
+ # anything to do
149
+ self._enabled = bool(self._quotes or self._dialog or self._alternate)
150
+
151
+ # Build narrator break RegExes
152
+ if narrator := CONFIG.narratorBreak.strip()[:1]:
153
+ punct = re.escape(".,:;!?")
154
+ self._breakD = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?|$)", re.UNICODE)
155
+ self._breakQ = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?)", re.UNICODE)
156
+ self._narrator = narrator
157
+ self._mode = f" {narrator}"
158
+
159
+ return
160
+
161
+ def __call__(self, text: str) -> list[tuple[int, int]]:
162
+ """Caller wrapper for dialogue processing."""
163
+ temp: list[int] = []
164
+ result: list[tuple[int, int]] = []
165
+ if text:
166
+ plain = True
167
+ if self._dialog and text[0] in self._dialog:
168
+ # The whole line is dialogue
169
+ plain = False
170
+ temp.append(0)
171
+ temp.append(len(text))
172
+ if self._breakD:
173
+ # Process narrator breaks in the dialogue
174
+ for res in self._breakD.finditer(text, 1):
175
+ temp.append(res.start(0))
176
+ temp.append(res.end(0))
177
+ elif self._quotes:
178
+ # Quoted dialogue is enabled, so we look for them
179
+ for res in self._quotes.finditer(text):
180
+ plain = False
181
+ temp.append(res.start(0))
182
+ temp.append(res.end(0))
183
+ if self._breakQ:
184
+ for sub in self._breakQ.finditer(text, res.start(0), res.end(0)):
185
+ temp.append(sub.start(0))
186
+ temp.append(sub.end(0))
187
+
188
+ if plain and self._alternate:
189
+ # The main rules found no dialogue, so we check for
190
+ # alternating dialogue sections, if enabled
191
+ pos = 0
192
+ for num, bit in enumerate(text.split(self._alternate)):
193
+ length = len(bit) + (1 if num > 0 else 0)
194
+ if num%2:
195
+ temp.append(pos)
196
+ temp.append(pos + length)
197
+ pos += length
198
+
199
+ if temp:
200
+ # Sort unique edges in increasing order, and add them in pairs
201
+ start = None
202
+ for pos in sorted(set(temp)):
203
+ if start is None:
204
+ start = pos
205
+ else:
206
+ result.append((start, pos))
207
+ start = None
208
+
209
+ return result
@@ -333,7 +333,7 @@ class GuiManuscriptBuild(NDialog):
333
333
  docBuild.queueAll()
334
334
 
335
335
  self.buildProgress.setMaximum(len(docBuild))
336
- for i, _ in docBuild.iterBuild(buildPath, bFormat):
336
+ for i, _ in docBuild.iterBuildDocument(buildPath, bFormat):
337
337
  self.buildProgress.setValue(i+1)
338
338
 
339
339
  self._build.setLastBuildPath(bPath)