novelWriter 2.5b1__py3-none-any.whl → 2.5rc1__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.5b1.dist-info → novelWriter-2.5rc1.dist-info}/METADATA +1 -1
- {novelWriter-2.5b1.dist-info → novelWriter-2.5rc1.dist-info}/RECORD +61 -61
- {novelWriter-2.5b1.dist-info → novelWriter-2.5rc1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +3 -3
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/project_pt_BR.json +74 -74
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/themes/cyberpunk_night.conf +1 -0
- novelwriter/assets/themes/default_dark.conf +1 -0
- novelwriter/assets/themes/default_light.conf +1 -0
- novelwriter/assets/themes/dracula.conf +1 -0
- novelwriter/assets/themes/solarized_dark.conf +1 -0
- novelwriter/assets/themes/solarized_light.conf +1 -0
- novelwriter/common.py +2 -3
- novelwriter/config.py +67 -15
- novelwriter/constants.py +8 -10
- novelwriter/core/buildsettings.py +5 -3
- novelwriter/core/coretools.py +3 -1
- novelwriter/core/docbuild.py +1 -0
- novelwriter/core/tohtml.py +69 -29
- novelwriter/core/tokenizer.py +83 -14
- novelwriter/core/toodt.py +48 -21
- novelwriter/core/toqdoc.py +25 -9
- novelwriter/dialogs/about.py +10 -15
- novelwriter/dialogs/docmerge.py +16 -16
- novelwriter/dialogs/docsplit.py +16 -16
- novelwriter/dialogs/editlabel.py +6 -8
- novelwriter/dialogs/preferences.py +94 -68
- novelwriter/dialogs/projectsettings.py +10 -10
- novelwriter/dialogs/quotes.py +9 -5
- novelwriter/dialogs/wordlist.py +6 -6
- novelwriter/enum.py +4 -5
- novelwriter/extensions/configlayout.py +23 -4
- novelwriter/extensions/modified.py +22 -3
- novelwriter/extensions/{circularprogress.py → progressbars.py} +26 -3
- novelwriter/extensions/statusled.py +28 -22
- novelwriter/gui/doceditor.py +20 -11
- novelwriter/gui/dochighlight.py +30 -39
- novelwriter/gui/docviewer.py +21 -14
- novelwriter/gui/mainmenu.py +11 -11
- novelwriter/gui/outline.py +3 -3
- novelwriter/gui/projtree.py +19 -28
- novelwriter/gui/search.py +10 -1
- novelwriter/gui/statusbar.py +25 -29
- novelwriter/gui/theme.py +3 -0
- novelwriter/guimain.py +91 -84
- novelwriter/shared.py +10 -8
- novelwriter/text/patterns.py +113 -0
- novelwriter/tools/dictionaries.py +2 -8
- novelwriter/tools/lipsum.py +8 -12
- novelwriter/tools/manusbuild.py +9 -9
- novelwriter/tools/manuscript.py +10 -5
- novelwriter/tools/manussettings.py +7 -3
- novelwriter/tools/noveldetails.py +10 -10
- novelwriter/tools/welcome.py +10 -10
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +5 -2
- novelwriter/extensions/simpleprogress.py +0 -53
- {novelWriter-2.5b1.dist-info → novelWriter-2.5rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5b1.dist-info → novelWriter-2.5rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5b1.dist-info → novelWriter-2.5rc1.dist-info}/top_level.txt +0 -0
novelwriter/constants.py
CHANGED
@@ -57,17 +57,14 @@ class nwConst:
|
|
57
57
|
STATUS_MSG_TIMEOUT = 15000 # milliseconds
|
58
58
|
MAX_SEARCH_RESULT = 1000
|
59
59
|
|
60
|
-
# Dialogs
|
61
|
-
DLG_FINISHED = 2
|
62
|
-
|
63
60
|
|
64
61
|
class nwRegEx:
|
65
62
|
|
66
63
|
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
67
|
-
FMT_EB = r"(?<![\w\\])(
|
68
|
-
FMT_ST = r"(?<![\w\\])(
|
69
|
-
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|
|
70
|
-
FMT_SV = r"(?<!\\)(\[(
|
64
|
+
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
65
|
+
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
|
66
|
+
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
|
67
|
+
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
|
71
68
|
|
72
69
|
|
73
70
|
class nwShortcode:
|
@@ -107,6 +104,7 @@ class nwFiles:
|
|
107
104
|
# Config Files
|
108
105
|
CONF_FILE = "novelwriter.conf"
|
109
106
|
RECENT_FILE = "recentProjects.json"
|
107
|
+
RECENT_PATH = "recentPaths.json"
|
110
108
|
|
111
109
|
# Project Root Files
|
112
110
|
PROJ_FILE = "nwProject.nwx"
|
@@ -414,7 +412,7 @@ class nwUnicode:
|
|
414
412
|
U_EMDASH = "\u2014" # Long dash
|
415
413
|
U_HBAR = "\u2015" # Horizontal bar
|
416
414
|
U_HELLIP = "\u2026" # Ellipsis
|
417
|
-
|
415
|
+
U_MAPOS = "\u02bc" # Modifier letter single apostrophe
|
418
416
|
U_PRIME = "\u2032" # Prime
|
419
417
|
U_DPRIME = "\u2033" # Double prime
|
420
418
|
|
@@ -481,7 +479,7 @@ class nwUnicode:
|
|
481
479
|
H_EMDASH = "—"
|
482
480
|
H_HBAR = "―"
|
483
481
|
H_HELLIP = "…"
|
484
|
-
|
482
|
+
H_MAPOS = "ʼ"
|
485
483
|
H_PRIME = "′"
|
486
484
|
H_DPRIME = "″"
|
487
485
|
|
@@ -546,7 +544,7 @@ class nwHtmlUnicode():
|
|
546
544
|
nwUnicode.U_EMDASH: nwUnicode.H_EMDASH,
|
547
545
|
nwUnicode.U_HBAR: nwUnicode.H_HBAR,
|
548
546
|
nwUnicode.U_HELLIP: nwUnicode.H_HELLIP,
|
549
|
-
nwUnicode.
|
547
|
+
nwUnicode.U_MAPOS: nwUnicode.H_MAPOS,
|
550
548
|
nwUnicode.U_PRIME: nwUnicode.H_PRIME,
|
551
549
|
nwUnicode.U_DPRIME: nwUnicode.H_DPRIME,
|
552
550
|
|
@@ -82,6 +82,7 @@ SETTINGS_TEMPLATE = {
|
|
82
82
|
"format.stripUnicode": (bool, False),
|
83
83
|
"format.replaceTabs": (bool, False),
|
84
84
|
"format.keepBreaks": (bool, True),
|
85
|
+
"format.showDialogue": (bool, False),
|
85
86
|
"format.firstLineIndent": (bool, False),
|
86
87
|
"format.firstIndentWidth": (float, 1.4),
|
87
88
|
"format.indentFirstPar": (bool, False),
|
@@ -131,6 +132,7 @@ SETTINGS_LABELS = {
|
|
131
132
|
"format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
|
132
133
|
"format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
|
133
134
|
"format.keepBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
|
135
|
+
"format.showDialogue": QT_TRANSLATE_NOOP("Builds", "Apply Dialogue Highlighting"),
|
134
136
|
|
135
137
|
"format.grpParIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
|
136
138
|
"format.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "Enable Indent"),
|
@@ -217,7 +219,7 @@ class BuildSettings:
|
|
217
219
|
return self._order
|
218
220
|
|
219
221
|
@property
|
220
|
-
def
|
222
|
+
def lastBuildPath(self) -> Path:
|
221
223
|
"""The last used build path."""
|
222
224
|
if self._path.is_dir():
|
223
225
|
return self._path
|
@@ -291,7 +293,7 @@ class BuildSettings:
|
|
291
293
|
self._order = value
|
292
294
|
return
|
293
295
|
|
294
|
-
def
|
296
|
+
def setLastBuildPath(self, path: Path | str | None) -> None:
|
295
297
|
"""Set the last used build path."""
|
296
298
|
if isinstance(path, str):
|
297
299
|
path = Path(path)
|
@@ -459,7 +461,7 @@ class BuildSettings:
|
|
459
461
|
self.setName(data.get("name", ""))
|
460
462
|
self.setBuildID(data.get("uuid", ""))
|
461
463
|
self.setOrder(data.get("order", 0))
|
462
|
-
self.
|
464
|
+
self.setLastBuildPath(data.get("path", None))
|
463
465
|
self.setLastBuildName(data.get("build", ""))
|
464
466
|
|
465
467
|
buildFmt = str(data.get("format", ""))
|
novelwriter/core/coretools.py
CHANGED
@@ -348,7 +348,9 @@ class DocSearch:
|
|
348
348
|
rxMatch = rxItt.next()
|
349
349
|
pos = rxMatch.capturedStart()
|
350
350
|
num = rxMatch.capturedLength()
|
351
|
-
|
351
|
+
lim = text[:pos].rfind("\n") + 1
|
352
|
+
cut = text[lim:pos].rfind(" ") + lim + 1
|
353
|
+
context = text[cut:cut+100].partition("\n")[0]
|
352
354
|
if context:
|
353
355
|
results.append((pos, num, context))
|
354
356
|
count += 1
|
novelwriter/core/docbuild.py
CHANGED
@@ -338,6 +338,7 @@ class NWBuildDocument:
|
|
338
338
|
bldObj.setJustify(self._build.getBool("format.justifyText"))
|
339
339
|
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
|
340
340
|
bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
|
341
|
+
bldObj.setDialogueHighlight(self._build.getBool("format.showDialogue"))
|
341
342
|
bldObj.setFirstLineIndent(
|
342
343
|
self._build.getBool("format.firstLineIndent"),
|
343
344
|
self._build.getFloat("format.firstIndentWidth"),
|
novelwriter/core/tohtml.py
CHANGED
@@ -29,7 +29,6 @@ import logging
|
|
29
29
|
from pathlib import Path
|
30
30
|
from time import time
|
31
31
|
|
32
|
-
from novelwriter import CONFIG
|
33
32
|
from novelwriter.common import formatTimeStamp
|
34
33
|
from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
|
35
34
|
from novelwriter.core.project import NWProject
|
@@ -38,24 +37,35 @@ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
|
|
38
37
|
|
39
38
|
logger = logging.getLogger(__name__)
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
Tokenizer.FMT_B_E
|
44
|
-
Tokenizer.FMT_I_B:
|
45
|
-
Tokenizer.
|
46
|
-
Tokenizer.
|
47
|
-
Tokenizer.
|
48
|
-
Tokenizer.
|
49
|
-
Tokenizer.
|
50
|
-
Tokenizer.
|
51
|
-
Tokenizer.
|
52
|
-
Tokenizer.FMT_SUP_B: "<sup>",
|
53
|
-
Tokenizer.FMT_SUP_E: "</sup>",
|
54
|
-
Tokenizer.FMT_SUB_B: "<sub>",
|
55
|
-
Tokenizer.FMT_SUB_E: "</sub>",
|
56
|
-
Tokenizer.FMT_STRIP: "",
|
40
|
+
# Each opener tag, with the id of its corresponding closer and tag format
|
41
|
+
HTML_OPENER: dict[int, tuple[int, str]] = {
|
42
|
+
Tokenizer.FMT_B_B: (Tokenizer.FMT_B_E, "<strong>"),
|
43
|
+
Tokenizer.FMT_I_B: (Tokenizer.FMT_I_E, "<em>"),
|
44
|
+
Tokenizer.FMT_D_B: (Tokenizer.FMT_D_E, "<del>"),
|
45
|
+
Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, "<span style='text-decoration: underline;'>"),
|
46
|
+
Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, "<mark>"),
|
47
|
+
Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, "<sup>"),
|
48
|
+
Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, "<sub>"),
|
49
|
+
Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, "<span class='dialog'>"),
|
50
|
+
Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, "<span class='altdialog'>"),
|
57
51
|
}
|
58
52
|
|
53
|
+
# Each closer tag, with the id of its corresponding opener and tag format
|
54
|
+
HTML_CLOSER: dict[int, tuple[int, str]] = {
|
55
|
+
Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, "</strong>"),
|
56
|
+
Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, "</em>"),
|
57
|
+
Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, "</del>"),
|
58
|
+
Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, "</span>"),
|
59
|
+
Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, "</mark>"),
|
60
|
+
Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, "</sup>"),
|
61
|
+
Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, "</sub>"),
|
62
|
+
Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, "</span>"),
|
63
|
+
Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, "</span>"),
|
64
|
+
}
|
65
|
+
|
66
|
+
# Empty HTML tag record
|
67
|
+
HTML_NONE = (0, "")
|
68
|
+
|
59
69
|
|
60
70
|
class ToHtml(Tokenizer):
|
61
71
|
"""Core: HTML Document Writer
|
@@ -184,7 +194,6 @@ class ToHtml(Tokenizer):
|
|
184
194
|
|
185
195
|
if tStyle & self.A_PBB:
|
186
196
|
aStyle.append("page-break-before: always;")
|
187
|
-
|
188
197
|
if tStyle & self.A_PBA:
|
189
198
|
aStyle.append("page-break-after: always;")
|
190
199
|
|
@@ -194,11 +203,13 @@ class ToHtml(Tokenizer):
|
|
194
203
|
aStyle.append("margin-top: 0;")
|
195
204
|
|
196
205
|
if tStyle & self.A_IND_L:
|
197
|
-
aStyle.append(f"margin-left: {
|
206
|
+
aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
|
198
207
|
if tStyle & self.A_IND_R:
|
199
|
-
aStyle.append(f"margin-right: {
|
208
|
+
aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
|
209
|
+
if tStyle & self.A_IND_T:
|
210
|
+
aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
|
200
211
|
|
201
|
-
if
|
212
|
+
if aStyle:
|
202
213
|
stVals = " ".join(aStyle)
|
203
214
|
hStyle = f" style='{stVals}'"
|
204
215
|
else:
|
@@ -431,6 +442,8 @@ class ToHtml(Tokenizer):
|
|
431
442
|
styles.append(".break {text-align: left;}")
|
432
443
|
styles.append(".synopsis {font-style: italic;}")
|
433
444
|
styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
|
445
|
+
styles.append(".dialog {color: rgb(66, 113, 174);}")
|
446
|
+
styles.append(".altdialog {color: rgb(129, 55, 9);}")
|
434
447
|
|
435
448
|
return styles
|
436
449
|
|
@@ -441,19 +454,46 @@ class ToHtml(Tokenizer):
|
|
441
454
|
def _formatText(self, text: str, tFmt: T_Formats) -> str:
|
442
455
|
"""Apply formatting tags to text."""
|
443
456
|
temp = text
|
444
|
-
|
445
|
-
|
446
|
-
|
457
|
+
|
458
|
+
# Build a list of all html tags that need to be inserted in the text.
|
459
|
+
# This is done in the forward direction, and a tag is only opened if it
|
460
|
+
# isn't already open, and only closed if it has previously been opened.
|
461
|
+
tags: list[tuple[int, str]] = []
|
462
|
+
state = dict.fromkeys(HTML_OPENER, False)
|
463
|
+
for pos, fmt, data in tFmt:
|
464
|
+
if m := HTML_OPENER.get(fmt):
|
465
|
+
if not state.get(fmt, True):
|
466
|
+
tags.append((pos, m[1]))
|
467
|
+
state[fmt] = True
|
468
|
+
elif m := HTML_CLOSER.get(fmt):
|
469
|
+
if state.get(m[0], False):
|
470
|
+
tags.append((pos, m[1]))
|
471
|
+
state[m[0]] = False
|
472
|
+
elif fmt == self.FMT_FNOTE:
|
447
473
|
if data in self._footnotes:
|
448
474
|
index = len(self._usedNotes) + 1
|
449
475
|
self._usedNotes[data] = index
|
450
|
-
|
476
|
+
tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
|
451
477
|
else:
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
478
|
+
tags.append((pos, "<sup>ERR</sup>"))
|
479
|
+
|
480
|
+
# Check all format types and close any tag that is still open. This
|
481
|
+
# ensures that unclosed tags don't spill over to the next paragraph.
|
482
|
+
end = len(text)
|
483
|
+
for opener, active in state.items():
|
484
|
+
if active:
|
485
|
+
closer = HTML_OPENER.get(opener, HTML_NONE)[0]
|
486
|
+
tags.append((end, HTML_CLOSER.get(closer, HTML_NONE)[1]))
|
487
|
+
|
488
|
+
# Insert all tags at their correct position, starting from the back.
|
489
|
+
# The reverse order ensures that the positions are not shifted while we
|
490
|
+
# insert tags.
|
491
|
+
for pos, tag in reversed(tags):
|
492
|
+
temp = f"{temp[:pos]}{tag}{temp[pos:]}"
|
493
|
+
|
494
|
+
# Replace all line breaks with proper HTML break tags
|
456
495
|
temp = temp.replace("\n", "<br>")
|
496
|
+
|
457
497
|
return stripEscape(temp)
|
458
498
|
|
459
499
|
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
|
novelwriter/core/tokenizer.py
CHANGED
@@ -36,13 +36,13 @@ from time import time
|
|
36
36
|
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
37
37
|
from PyQt5.QtGui import QFont
|
38
38
|
|
39
|
+
from novelwriter import CONFIG
|
39
40
|
from novelwriter.common import checkInt, formatTimeStamp, numberToRoman
|
40
|
-
from novelwriter.constants import
|
41
|
-
nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
|
42
|
-
)
|
41
|
+
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
|
43
42
|
from novelwriter.core.index import processComment
|
44
43
|
from novelwriter.core.project import NWProject
|
45
44
|
from novelwriter.enum import nwComment, nwItemLayout
|
45
|
+
from novelwriter.text.patterns import REGEX_PATTERNS
|
46
46
|
|
47
47
|
logger = logging.getLogger(__name__)
|
48
48
|
|
@@ -85,8 +85,12 @@ class Tokenizer(ABC):
|
|
85
85
|
FMT_SUP_E = 12 # End superscript
|
86
86
|
FMT_SUB_B = 13 # Begin subscript
|
87
87
|
FMT_SUB_E = 14 # End subscript
|
88
|
-
|
89
|
-
|
88
|
+
FMT_DL_B = 15 # Begin dialogue
|
89
|
+
FMT_DL_E = 16 # End dialogue
|
90
|
+
FMT_ADL_B = 17 # Begin alt dialogue
|
91
|
+
FMT_ADL_E = 18 # End alt dialogue
|
92
|
+
FMT_FNOTE = 19 # Footnote marker
|
93
|
+
FMT_STRIP = 20 # Strip the format code
|
90
94
|
|
91
95
|
# Block Type
|
92
96
|
T_EMPTY = 1 # Empty line (new paragraph)
|
@@ -115,6 +119,10 @@ class Tokenizer(ABC):
|
|
115
119
|
A_Z_BTMMRG = 0x0080 # Zero bottom margin
|
116
120
|
A_IND_L = 0x0100 # Left indentation
|
117
121
|
A_IND_R = 0x0200 # Right indentation
|
122
|
+
A_IND_T = 0x0400 # Text indentation
|
123
|
+
|
124
|
+
# Masks
|
125
|
+
M_ALIGNED = A_LEFT | A_RIGHT | A_CENTRE | A_JUSTIFY
|
118
126
|
|
119
127
|
# Lookups
|
120
128
|
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
|
@@ -189,7 +197,8 @@ class Tokenizer(ABC):
|
|
189
197
|
|
190
198
|
# Instance Variables
|
191
199
|
self._hFormatter = HeadingFormatter(self._project)
|
192
|
-
self._noSep = True
|
200
|
+
self._noSep = True # Flag to indicate that we don't want a scene separator
|
201
|
+
self._showDialog = False # Flag for dialogue highlighting
|
193
202
|
|
194
203
|
# This File
|
195
204
|
self._isNovel = False # Document is a novel document
|
@@ -204,12 +213,12 @@ class Tokenizer(ABC):
|
|
204
213
|
|
205
214
|
# Format RegEx
|
206
215
|
self._rxMarkdown = [
|
207
|
-
(
|
208
|
-
(
|
209
|
-
(
|
216
|
+
(REGEX_PATTERNS.markdownItalic, [0, self.FMT_I_B, 0, self.FMT_I_E]),
|
217
|
+
(REGEX_PATTERNS.markdownBold, [0, self.FMT_B_B, 0, self.FMT_B_E]),
|
218
|
+
(REGEX_PATTERNS.markdownStrike, [0, self.FMT_D_B, 0, self.FMT_D_E]),
|
210
219
|
]
|
211
|
-
self._rxShortCodes =
|
212
|
-
self._rxShortCodeVals =
|
220
|
+
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
|
221
|
+
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
|
213
222
|
|
214
223
|
self._shortCodeFmt = {
|
215
224
|
nwShortcode.ITALIC_O: self.FMT_I_B, nwShortcode.ITALIC_C: self.FMT_I_E,
|
@@ -224,6 +233,8 @@ class Tokenizer(ABC):
|
|
224
233
|
nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
|
225
234
|
}
|
226
235
|
|
236
|
+
self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
|
237
|
+
|
227
238
|
return
|
228
239
|
|
229
240
|
##
|
@@ -345,6 +356,29 @@ class Tokenizer(ABC):
|
|
345
356
|
self._doJustify = state
|
346
357
|
return
|
347
358
|
|
359
|
+
def setDialogueHighlight(self, state: bool) -> None:
|
360
|
+
"""Enable or disable dialogue highlighting."""
|
361
|
+
self._rxDialogue = []
|
362
|
+
self._showDialog = state
|
363
|
+
if state:
|
364
|
+
if CONFIG.dialogStyle > 0:
|
365
|
+
self._rxDialogue.append((
|
366
|
+
REGEX_PATTERNS.dialogStyle, self.FMT_DL_B, self.FMT_DL_E
|
367
|
+
))
|
368
|
+
if CONFIG.dialogLine:
|
369
|
+
self._rxDialogue.append((
|
370
|
+
REGEX_PATTERNS.dialogLine, self.FMT_DL_B, self.FMT_DL_E
|
371
|
+
))
|
372
|
+
if CONFIG.narratorBreak:
|
373
|
+
self._rxDialogue.append((
|
374
|
+
REGEX_PATTERNS.narratorBreak, self.FMT_DL_E, self.FMT_DL_B
|
375
|
+
))
|
376
|
+
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
377
|
+
self._rxDialogue.append((
|
378
|
+
REGEX_PATTERNS.altDialogStyle, self.FMT_ADL_B, self.FMT_ADL_E
|
379
|
+
))
|
380
|
+
return
|
381
|
+
|
348
382
|
def setTitleMargins(self, upper: float, lower: float) -> None:
|
349
383
|
"""Set the upper and lower title margin."""
|
350
384
|
self._marginTitle = (float(upper), float(lower))
|
@@ -481,7 +515,7 @@ class Tokenizer(ABC):
|
|
481
515
|
self._text = xRep.sub(lambda x: repDict[x.group(0)], self._text)
|
482
516
|
|
483
517
|
# Process the character translation map
|
484
|
-
trDict = {nwUnicode.
|
518
|
+
trDict = {nwUnicode.U_MAPOS: nwUnicode.U_RSQUO}
|
485
519
|
self._text = self._text.translate(str.maketrans(trDict))
|
486
520
|
|
487
521
|
return
|
@@ -839,6 +873,7 @@ class Tokenizer(ABC):
|
|
839
873
|
pLines: list[T_Token] = []
|
840
874
|
|
841
875
|
tCount = len(tokens)
|
876
|
+
pIndent = True
|
842
877
|
for n, cToken in enumerate(tokens):
|
843
878
|
|
844
879
|
if n > 0:
|
@@ -846,6 +881,12 @@ class Tokenizer(ABC):
|
|
846
881
|
if n < tCount - 1:
|
847
882
|
nToken = tokens[n+1] # Look ahead
|
848
883
|
|
884
|
+
if not self._indentFirst and cToken[0] in self.L_SKIP_INDENT:
|
885
|
+
# Unless the indentFirst flag is set, we set up the next
|
886
|
+
# paragraph to not be indented if we see a block of a
|
887
|
+
# specific type
|
888
|
+
pIndent = False
|
889
|
+
|
849
890
|
if cToken[0] == self.T_EMPTY:
|
850
891
|
# We don't need to keep the empty lines after this pass
|
851
892
|
pass
|
@@ -864,11 +905,27 @@ class Tokenizer(ABC):
|
|
864
905
|
elif cToken[0] == self.T_TEXT:
|
865
906
|
# Combine lines from the same paragraph
|
866
907
|
pLines.append(cToken)
|
908
|
+
|
867
909
|
if nToken[0] != self.T_TEXT:
|
910
|
+
# Next token is not text, so we add the buffer to tokens
|
868
911
|
nLines = len(pLines)
|
912
|
+
cStyle = pLines[0][4]
|
913
|
+
if self._firstIndent and pIndent and not cStyle & self.M_ALIGNED:
|
914
|
+
# If paragraph indentation is enabled, not temporarily
|
915
|
+
# turned off, and the block is not aligned, we add the
|
916
|
+
# text indentation flag
|
917
|
+
cStyle |= self.A_IND_T
|
918
|
+
|
869
919
|
if nLines == 1:
|
870
|
-
|
920
|
+
# The paragraph contains a single line, so we just
|
921
|
+
# save that directly to the token list
|
922
|
+
self._tokens.append((
|
923
|
+
self.T_TEXT, pLines[0][1], pLines[0][2], pLines[0][3], cStyle
|
924
|
+
))
|
871
925
|
elif nLines > 1:
|
926
|
+
# The paragraph contains multiple lines, so we need to
|
927
|
+
# join them according to the line break policy, and
|
928
|
+
# recompute all the formatting markers
|
872
929
|
tTxt = ""
|
873
930
|
tFmt: T_Formats = []
|
874
931
|
for aToken in pLines:
|
@@ -876,9 +933,12 @@ class Tokenizer(ABC):
|
|
876
933
|
tTxt += f"{aToken[2]}{lineSep}"
|
877
934
|
tFmt.extend((p+tLen, fmt, key) for p, fmt, key in aToken[3])
|
878
935
|
self._tokens.append((
|
879
|
-
self.T_TEXT, pLines[0][1], tTxt[:-1], tFmt,
|
936
|
+
self.T_TEXT, pLines[0][1], tTxt[:-1], tFmt, cStyle
|
880
937
|
))
|
938
|
+
|
939
|
+
# Reset buffer and make sure text indent is on for next pass
|
881
940
|
pLines = []
|
941
|
+
pIndent = True
|
882
942
|
|
883
943
|
else:
|
884
944
|
self._tokens.append(cToken)
|
@@ -1076,6 +1136,15 @@ class Tokenizer(ABC):
|
|
1076
1136
|
f"{tHandle}:{rxMatch.captured(2)}",
|
1077
1137
|
))
|
1078
1138
|
|
1139
|
+
# Match Dialogue
|
1140
|
+
if self._rxDialogue:
|
1141
|
+
for regEx, fmtB, fmtE in self._rxDialogue:
|
1142
|
+
rxItt = regEx.globalMatch(text, 0)
|
1143
|
+
while rxItt.hasNext():
|
1144
|
+
rxMatch = rxItt.next()
|
1145
|
+
temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
|
1146
|
+
temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))
|
1147
|
+
|
1079
1148
|
# Post-process text and format
|
1080
1149
|
result = text
|
1081
1150
|
formats = []
|
novelwriter/core/toodt.py
CHANGED
@@ -82,13 +82,15 @@ TAG_SPAN = _mkTag("text", "span")
|
|
82
82
|
TAG_STNM = _mkTag("text", "style-name")
|
83
83
|
|
84
84
|
# Formatting Codes
|
85
|
-
X_BLD =
|
86
|
-
X_ITA =
|
87
|
-
X_DEL =
|
88
|
-
X_UND =
|
89
|
-
X_MRK =
|
90
|
-
X_SUP =
|
91
|
-
X_SUB =
|
85
|
+
X_BLD = 0x001 # Bold format
|
86
|
+
X_ITA = 0x002 # Italic format
|
87
|
+
X_DEL = 0x004 # Strikethrough format
|
88
|
+
X_UND = 0x008 # Underline format
|
89
|
+
X_MRK = 0x010 # Marked format
|
90
|
+
X_SUP = 0x020 # Superscript
|
91
|
+
X_SUB = 0x040 # Subscript
|
92
|
+
X_DLG = 0x080 # Dialogue
|
93
|
+
X_DLA = 0x100 # Alt. Dialogue
|
92
94
|
|
93
95
|
# Formatting Masks
|
94
96
|
M_BLD = ~X_BLD
|
@@ -98,6 +100,8 @@ M_UND = ~X_UND
|
|
98
100
|
M_MRK = ~X_MRK
|
99
101
|
M_SUP = ~X_SUP
|
100
102
|
M_SUB = ~X_SUB
|
103
|
+
M_DLG = ~X_DLG
|
104
|
+
M_DLA = ~X_DLA
|
101
105
|
|
102
106
|
# ODT Styles
|
103
107
|
S_TITLE = "Title"
|
@@ -216,13 +220,15 @@ class ToOdt(Tokenizer):
|
|
216
220
|
self._mDocRight = "2.000cm"
|
217
221
|
|
218
222
|
# Colour
|
219
|
-
self._colHead12
|
220
|
-
self._opaHead12
|
221
|
-
self._colHead34
|
222
|
-
self._opaHead34
|
223
|
-
self._colMetaTx
|
224
|
-
self._opaMetaTx
|
225
|
-
self.
|
223
|
+
self._colHead12 = None
|
224
|
+
self._opaHead12 = None
|
225
|
+
self._colHead34 = None
|
226
|
+
self._opaHead34 = None
|
227
|
+
self._colMetaTx = None
|
228
|
+
self._opaMetaTx = None
|
229
|
+
self._colDialogM = None
|
230
|
+
self._colDialogA = None
|
231
|
+
self._markText = "#ffffa6"
|
226
232
|
|
227
233
|
return
|
228
234
|
|
@@ -324,6 +330,10 @@ class ToOdt(Tokenizer):
|
|
324
330
|
self._colMetaTx = "#813709"
|
325
331
|
self._opaMetaTx = "100%"
|
326
332
|
|
333
|
+
if self._showDialog:
|
334
|
+
self._colDialogM = "#2a6099"
|
335
|
+
self._colDialogA = "#813709"
|
336
|
+
|
327
337
|
self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
|
328
338
|
self._fBlockIndent = self._emToCm(self._blockIndent)
|
329
339
|
self._fTextIndent = self._emToCm(self._firstWidth)
|
@@ -438,7 +448,6 @@ class ToOdt(Tokenizer):
|
|
438
448
|
self._result = "" # Not used, but cleared just in case
|
439
449
|
|
440
450
|
xText = self._xText
|
441
|
-
pIndent = True
|
442
451
|
for tType, _, tText, tFormat, tStyle in self._tokens:
|
443
452
|
|
444
453
|
# Styles
|
@@ -455,7 +464,6 @@ class ToOdt(Tokenizer):
|
|
455
464
|
|
456
465
|
if tStyle & self.A_PBB:
|
457
466
|
oStyle.setBreakBefore("page")
|
458
|
-
|
459
467
|
if tStyle & self.A_PBA:
|
460
468
|
oStyle.setBreakAfter("page")
|
461
469
|
|
@@ -469,16 +477,14 @@ class ToOdt(Tokenizer):
|
|
469
477
|
if tStyle & self.A_IND_R:
|
470
478
|
oStyle.setMarginRight(self._fBlockIndent)
|
471
479
|
|
472
|
-
if not self._indentFirst and tType in self.L_SKIP_INDENT:
|
473
|
-
pIndent = False
|
474
|
-
|
475
480
|
# Process Text Types
|
476
481
|
if tType == self.T_TEXT:
|
477
|
-
|
482
|
+
# Text indentation is processed here because there is a
|
483
|
+
# dedicated pre-defined style for it
|
484
|
+
if tStyle & self.A_IND_T:
|
478
485
|
self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
|
479
486
|
else:
|
480
487
|
self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
|
481
|
-
pIndent = True
|
482
488
|
|
483
489
|
elif tType == self.T_TITLE:
|
484
490
|
# Title must be text:p
|
@@ -688,6 +694,14 @@ class ToOdt(Tokenizer):
|
|
688
694
|
xFmt |= X_SUB
|
689
695
|
elif fFmt == self.FMT_SUB_E:
|
690
696
|
xFmt &= M_SUB
|
697
|
+
elif fFmt == self.FMT_DL_B:
|
698
|
+
xFmt |= X_DLG
|
699
|
+
elif fFmt == self.FMT_DL_E:
|
700
|
+
xFmt &= M_DLG
|
701
|
+
elif fFmt == self.FMT_ADL_B:
|
702
|
+
xFmt |= X_DLA
|
703
|
+
elif fFmt == self.FMT_ADL_E:
|
704
|
+
xFmt &= M_DLA
|
691
705
|
elif fFmt == self.FMT_FNOTE:
|
692
706
|
xNode = self._generateFootnote(fData)
|
693
707
|
elif fFmt == self.FMT_STRIP:
|
@@ -761,6 +775,10 @@ class ToOdt(Tokenizer):
|
|
761
775
|
style.setTextPosition("super")
|
762
776
|
if hFmt & X_SUB:
|
763
777
|
style.setTextPosition("sub")
|
778
|
+
if hFmt & X_DLG:
|
779
|
+
style.setColour(self._colDialogM)
|
780
|
+
if hFmt & X_DLA:
|
781
|
+
style.setColour(self._colDialogA)
|
764
782
|
self._autoText[hFmt] = style
|
765
783
|
|
766
784
|
return style.name
|
@@ -1361,6 +1379,7 @@ class ODTTextStyle:
|
|
1361
1379
|
self._tAttr = {
|
1362
1380
|
"font-weight": ["fo", None],
|
1363
1381
|
"font-style": ["fo", None],
|
1382
|
+
"color": ["fo", None],
|
1364
1383
|
"background-color": ["fo", None],
|
1365
1384
|
"text-position": ["style", None],
|
1366
1385
|
"text-line-through-style": ["style", None],
|
@@ -1395,6 +1414,14 @@ class ODTTextStyle:
|
|
1395
1414
|
self._tAttr["font-style"][1] = None
|
1396
1415
|
return
|
1397
1416
|
|
1417
|
+
def setColour(self, value: str | None) -> None:
|
1418
|
+
"""Set text colour."""
|
1419
|
+
if value and len(value) == 7 and value[0] == "#":
|
1420
|
+
self._tAttr["color"][1] = value
|
1421
|
+
else:
|
1422
|
+
self._tAttr["color"][1] = None
|
1423
|
+
return
|
1424
|
+
|
1398
1425
|
def setBackgroundColour(self, value: str | None) -> None:
|
1399
1426
|
"""Set text background colour."""
|
1400
1427
|
if value and len(value) == 7 and value[0] == "#":
|