novelWriter 2.5b1__py3-none-any.whl → 2.5.1__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.5.1.dist-info}/METADATA +1 -1
- {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/RECORD +77 -75
- {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +3 -3
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_pl_PL.json +116 -0
- novelwriter/assets/i18n/project_pt_BR.json +74 -74
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/text/credits_en.htm +52 -44
- 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 +12 -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/project.py +15 -4
- novelwriter/core/status.py +4 -1
- novelwriter/core/storage.py +6 -1
- novelwriter/core/tohtml.py +69 -29
- novelwriter/core/tokenizer.py +83 -14
- novelwriter/core/toodt.py +48 -21
- novelwriter/core/toqdoc.py +37 -21
- 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 +106 -93
- novelwriter/dialogs/projectsettings.py +16 -20
- novelwriter/dialogs/quotes.py +9 -5
- novelwriter/dialogs/wordlist.py +6 -6
- novelwriter/enum.py +4 -5
- novelwriter/extensions/configlayout.py +38 -4
- novelwriter/extensions/modified.py +22 -3
- novelwriter/extensions/{circularprogress.py → progressbars.py} +26 -3
- novelwriter/extensions/statusled.py +39 -23
- novelwriter/gui/doceditor.py +22 -13
- novelwriter/gui/dochighlight.py +30 -39
- novelwriter/gui/docviewer.py +24 -15
- novelwriter/gui/docviewerpanel.py +7 -0
- novelwriter/gui/mainmenu.py +11 -11
- novelwriter/gui/outline.py +4 -3
- novelwriter/gui/projtree.py +85 -77
- novelwriter/gui/search.py +10 -1
- novelwriter/gui/statusbar.py +25 -29
- novelwriter/gui/theme.py +3 -0
- novelwriter/guimain.py +139 -124
- novelwriter/shared.py +19 -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 +19 -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.5.1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/top_level.txt +0 -0
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,9 @@ 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._noIndent = False # Flag to disable text indent on next paragraph
|
202
|
+
self._showDialog = False # Flag for dialogue highlighting
|
193
203
|
|
194
204
|
# This File
|
195
205
|
self._isNovel = False # Document is a novel document
|
@@ -204,12 +214,12 @@ class Tokenizer(ABC):
|
|
204
214
|
|
205
215
|
# Format RegEx
|
206
216
|
self._rxMarkdown = [
|
207
|
-
(
|
208
|
-
(
|
209
|
-
(
|
217
|
+
(REGEX_PATTERNS.markdownItalic, [0, self.FMT_I_B, 0, self.FMT_I_E]),
|
218
|
+
(REGEX_PATTERNS.markdownBold, [0, self.FMT_B_B, 0, self.FMT_B_E]),
|
219
|
+
(REGEX_PATTERNS.markdownStrike, [0, self.FMT_D_B, 0, self.FMT_D_E]),
|
210
220
|
]
|
211
|
-
self._rxShortCodes =
|
212
|
-
self._rxShortCodeVals =
|
221
|
+
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
|
222
|
+
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
|
213
223
|
|
214
224
|
self._shortCodeFmt = {
|
215
225
|
nwShortcode.ITALIC_O: self.FMT_I_B, nwShortcode.ITALIC_C: self.FMT_I_E,
|
@@ -224,6 +234,8 @@ class Tokenizer(ABC):
|
|
224
234
|
nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
|
225
235
|
}
|
226
236
|
|
237
|
+
self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
|
238
|
+
|
227
239
|
return
|
228
240
|
|
229
241
|
##
|
@@ -345,6 +357,29 @@ class Tokenizer(ABC):
|
|
345
357
|
self._doJustify = state
|
346
358
|
return
|
347
359
|
|
360
|
+
def setDialogueHighlight(self, state: bool) -> None:
|
361
|
+
"""Enable or disable dialogue highlighting."""
|
362
|
+
self._rxDialogue = []
|
363
|
+
self._showDialog = state
|
364
|
+
if state:
|
365
|
+
if CONFIG.dialogStyle > 0:
|
366
|
+
self._rxDialogue.append((
|
367
|
+
REGEX_PATTERNS.dialogStyle, self.FMT_DL_B, self.FMT_DL_E
|
368
|
+
))
|
369
|
+
if CONFIG.dialogLine:
|
370
|
+
self._rxDialogue.append((
|
371
|
+
REGEX_PATTERNS.dialogLine, self.FMT_DL_B, self.FMT_DL_E
|
372
|
+
))
|
373
|
+
if CONFIG.narratorBreak:
|
374
|
+
self._rxDialogue.append((
|
375
|
+
REGEX_PATTERNS.narratorBreak, self.FMT_DL_E, self.FMT_DL_B
|
376
|
+
))
|
377
|
+
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
378
|
+
self._rxDialogue.append((
|
379
|
+
REGEX_PATTERNS.altDialogStyle, self.FMT_ADL_B, self.FMT_ADL_E
|
380
|
+
))
|
381
|
+
return
|
382
|
+
|
348
383
|
def setTitleMargins(self, upper: float, lower: float) -> None:
|
349
384
|
"""Set the upper and lower title margin."""
|
350
385
|
self._marginTitle = (float(upper), float(lower))
|
@@ -481,7 +516,7 @@ class Tokenizer(ABC):
|
|
481
516
|
self._text = xRep.sub(lambda x: repDict[x.group(0)], self._text)
|
482
517
|
|
483
518
|
# Process the character translation map
|
484
|
-
trDict = {nwUnicode.
|
519
|
+
trDict = {nwUnicode.U_MAPOS: nwUnicode.U_RSQUO}
|
485
520
|
self._text = self._text.translate(str.maketrans(trDict))
|
486
521
|
|
487
522
|
return
|
@@ -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 cToken[0] in self.L_SKIP_INDENT and not self._indentFirst:
|
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
|
+
self._noIndent = True
|
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 not (self._noIndent or 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
|
+
self._noIndent = False
|
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] == "#":
|
novelwriter/core/toqdoc.py
CHANGED
@@ -26,7 +26,7 @@ from __future__ import annotations
|
|
26
26
|
import logging
|
27
27
|
|
28
28
|
from PyQt5.QtGui import (
|
29
|
-
QColor, QFont,
|
29
|
+
QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
|
30
30
|
QTextCursor, QTextDocument
|
31
31
|
)
|
32
32
|
|
@@ -45,16 +45,18 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
|
|
45
45
|
|
46
46
|
|
47
47
|
class TextDocumentTheme:
|
48
|
-
text:
|
48
|
+
text: QColor = QtBlack
|
49
49
|
highlight: QColor = QtTransparent
|
50
|
-
head:
|
51
|
-
comment:
|
52
|
-
note:
|
53
|
-
code:
|
54
|
-
modifier:
|
55
|
-
keyword:
|
56
|
-
tag:
|
57
|
-
optional:
|
50
|
+
head: QColor = QtBlack
|
51
|
+
comment: QColor = QtBlack
|
52
|
+
note: QColor = QtBlack
|
53
|
+
code: QColor = QtBlack
|
54
|
+
modifier: QColor = QtBlack
|
55
|
+
keyword: QColor = QtBlack
|
56
|
+
tag: QColor = QtBlack
|
57
|
+
optional: QColor = QtBlack
|
58
|
+
dialog: QColor = QtBlack
|
59
|
+
altdialog: QColor = QtBlack
|
58
60
|
|
59
61
|
|
60
62
|
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
|
@@ -97,19 +99,19 @@ class ToQTextDocument(Tokenizer):
|
|
97
99
|
self._document.clear()
|
98
100
|
self._document.setDefaultFont(self._textFont)
|
99
101
|
|
100
|
-
qMetric =
|
101
|
-
|
102
|
+
qMetric = QFontMetricsF(self._textFont)
|
103
|
+
mPx = qMetric.ascent() # 1 em in pixels
|
102
104
|
fPt = self._textFont.pointSizeF()
|
103
105
|
|
104
106
|
# Scaled Sizes
|
105
107
|
# ============
|
106
108
|
|
107
109
|
self._mHead = {
|
108
|
-
self.T_TITLE: (
|
109
|
-
self.T_HEAD1: (
|
110
|
-
self.T_HEAD2: (
|
111
|
-
self.T_HEAD3: (
|
112
|
-
self.T_HEAD4: (
|
110
|
+
self.T_TITLE: (mPx * self._marginTitle[0], mPx * self._marginTitle[1]),
|
111
|
+
self.T_HEAD1: (mPx * self._marginHead1[0], mPx * self._marginHead1[1]),
|
112
|
+
self.T_HEAD2: (mPx * self._marginHead2[0], mPx * self._marginHead2[1]),
|
113
|
+
self.T_HEAD3: (mPx * self._marginHead3[0], mPx * self._marginHead3[1]),
|
114
|
+
self.T_HEAD4: (mPx * self._marginHead4[0], mPx * self._marginHead4[1]),
|
113
115
|
}
|
114
116
|
|
115
117
|
self._sHead = {
|
@@ -120,11 +122,12 @@ class ToQTextDocument(Tokenizer):
|
|
120
122
|
self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
|
121
123
|
}
|
122
124
|
|
123
|
-
self._mText = (
|
124
|
-
self._mMeta = (
|
125
|
-
self._mSep = (
|
125
|
+
self._mText = (mPx * self._marginText[0], mPx * self._marginText[1])
|
126
|
+
self._mMeta = (mPx * self._marginMeta[0], mPx * self._marginMeta[1])
|
127
|
+
self._mSep = (mPx * self._marginSep[0], mPx * self._marginSep[1])
|
126
128
|
|
127
|
-
self._mIndent =
|
129
|
+
self._mIndent = mPx * 2.0
|
130
|
+
self._tIndent = mPx * self._firstWidth
|
128
131
|
|
129
132
|
# Block Format
|
130
133
|
# ============
|
@@ -133,6 +136,9 @@ class ToQTextDocument(Tokenizer):
|
|
133
136
|
self._blockFmt.setTopMargin(self._mText[0])
|
134
137
|
self._blockFmt.setBottomMargin(self._mText[1])
|
135
138
|
self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
|
139
|
+
self._blockFmt.setLineHeight(
|
140
|
+
100*self._lineHeight, QTextBlockFormat.LineHeightTypes.ProportionalHeight
|
141
|
+
)
|
136
142
|
|
137
143
|
# Character Formats
|
138
144
|
# =================
|
@@ -224,6 +230,8 @@ class ToQTextDocument(Tokenizer):
|
|
224
230
|
bFmt.setLeftMargin(self._mIndent)
|
225
231
|
if tStyle & self.A_IND_R:
|
226
232
|
bFmt.setRightMargin(self._mIndent)
|
233
|
+
if tStyle & self.A_IND_T:
|
234
|
+
bFmt.setTextIndent(self._tIndent)
|
227
235
|
|
228
236
|
if tType == self.T_TEXT:
|
229
237
|
newBlock(cursor, bFmt)
|
@@ -337,6 +345,14 @@ class ToQTextDocument(Tokenizer):
|
|
337
345
|
cFmt.setVerticalAlignment(QtVAlignSub)
|
338
346
|
elif fmt == self.FMT_SUB_E:
|
339
347
|
cFmt.setVerticalAlignment(QtVAlignNormal)
|
348
|
+
elif fmt == self.FMT_DL_B:
|
349
|
+
cFmt.setForeground(self._theme.dialog)
|
350
|
+
elif fmt == self.FMT_DL_E:
|
351
|
+
cFmt.setForeground(self._theme.text)
|
352
|
+
elif fmt == self.FMT_ADL_B:
|
353
|
+
cFmt.setForeground(self._theme.altdialog)
|
354
|
+
elif fmt == self.FMT_ADL_E:
|
355
|
+
cFmt.setForeground(self._theme.text)
|
340
356
|
elif fmt == self.FMT_FNOTE:
|
341
357
|
xFmt = QTextCharFormat(self._cCode)
|
342
358
|
xFmt.setVerticalAlignment(QtVAlignSuper)
|