novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/common.py +100 -2
- novelwriter/config.py +25 -15
- novelwriter/constants.py +168 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +145 -147
- novelwriter/core/docbuild.py +132 -170
- novelwriter/core/index.py +38 -37
- novelwriter/core/item.py +41 -8
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +4 -1
- novelwriter/core/project.py +67 -89
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +46 -33
- novelwriter/dialogs/projectsettings.py +5 -5
- novelwriter/enum.py +36 -23
- novelwriter/extensions/configlayout.py +27 -12
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +451 -0
- novelwriter/{core → formats}/tokenizer.py +487 -491
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +311 -432
- novelwriter/formats/toqdoc.py +484 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +342 -284
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +88 -31
- novelwriter/gui/docviewerpanel.py +17 -25
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +25 -28
- novelwriter/gui/mainmenu.py +129 -63
- novelwriter/gui/noveltree.py +45 -47
- novelwriter/gui/outline.py +196 -249
- novelwriter/gui/projtree.py +594 -1241
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +10 -5
- novelwriter/guimain.py +100 -196
- novelwriter/shared.py +66 -27
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +168 -60
- novelwriter/tools/manusbuild.py +14 -12
- novelwriter/tools/manuscript.py +120 -78
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +23 -7
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
novelwriter/tools/welcome.py
CHANGED
@@ -49,7 +49,7 @@ from novelwriter.extensions.configlayout import NWrappedWidgetBox
|
|
49
49
|
from novelwriter.extensions.modified import NDialog, NIconToolButton, NSpinBox
|
50
50
|
from novelwriter.extensions.switch import NSwitch
|
51
51
|
from novelwriter.extensions.versioninfo import VersionInfoWidget
|
52
|
-
from novelwriter.types import QtAlignLeft, QtAlignRightTop, QtSelected
|
52
|
+
from novelwriter.types import QtAlignLeft, QtAlignRightTop, QtScrollAsNeeded, QtSelected
|
53
53
|
|
54
54
|
logger = logging.getLogger(__name__)
|
55
55
|
|
@@ -297,7 +297,7 @@ class _OpenProjectPage(QWidget):
|
|
297
297
|
self.selectedPath.addAction(self.aMissing, QLineEdit.ActionPosition.TrailingPosition)
|
298
298
|
|
299
299
|
self.keyDelete = QShortcut(self)
|
300
|
-
self.keyDelete.setKey(
|
300
|
+
self.keyDelete.setKey("Del")
|
301
301
|
self.keyDelete.activated.connect(self._deleteSelectedItem)
|
302
302
|
|
303
303
|
# Assemble
|
@@ -501,8 +501,8 @@ class _NewProjectPage(QWidget):
|
|
501
501
|
self.scrollArea = QScrollArea(self)
|
502
502
|
self.scrollArea.setWidget(self.projectForm)
|
503
503
|
self.scrollArea.setWidgetResizable(True)
|
504
|
-
self.scrollArea.setHorizontalScrollBarPolicy(
|
505
|
-
self.scrollArea.setVerticalScrollBarPolicy(
|
504
|
+
self.scrollArea.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
505
|
+
self.scrollArea.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
506
506
|
|
507
507
|
self.enterForm = self.projectForm.enterForm
|
508
508
|
|
@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import (
|
|
38
38
|
)
|
39
39
|
|
40
40
|
from novelwriter import CONFIG, SHARED
|
41
|
-
from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax
|
41
|
+
from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax, qtLambda
|
42
42
|
from novelwriter.constants import nwConst
|
43
43
|
from novelwriter.error import formatException
|
44
44
|
from novelwriter.extensions.modified import NToolDialog
|
@@ -277,11 +277,11 @@ class GuiWritingStats(NToolDialog):
|
|
277
277
|
self.btnSave.setMenu(self.saveMenu)
|
278
278
|
|
279
279
|
self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self)
|
280
|
-
self.saveJSON.triggered.connect(
|
280
|
+
self.saveJSON.triggered.connect(qtLambda(self._saveData, self.FMT_JSON))
|
281
281
|
self.saveMenu.addAction(self.saveJSON)
|
282
282
|
|
283
283
|
self.saveCSV = QAction(self.tr("CSV Data File (.csv)"), self)
|
284
|
-
self.saveCSV.triggered.connect(
|
284
|
+
self.saveCSV.triggered.connect(qtLambda(self._saveData, self.FMT_CSV))
|
285
285
|
self.saveMenu.addAction(self.saveCSV)
|
286
286
|
|
287
287
|
# Assemble
|
novelwriter/types.py
CHANGED
@@ -23,9 +23,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
23
|
"""
|
24
24
|
from __future__ import annotations
|
25
25
|
|
26
|
-
from PyQt5.QtCore import
|
27
|
-
from PyQt5.QtGui import
|
28
|
-
|
26
|
+
from PyQt5.QtCore import Qt
|
27
|
+
from PyQt5.QtGui import (
|
28
|
+
QColor, QFont, QPainter, QTextBlockFormat, QTextCharFormat, QTextCursor,
|
29
|
+
QTextFormat
|
30
|
+
)
|
31
|
+
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QHeaderView, QSizePolicy, QStyle
|
29
32
|
|
30
33
|
# Qt Alignment Flags
|
31
34
|
|
@@ -48,11 +51,13 @@ QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
|
|
48
51
|
QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
|
49
52
|
QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
|
50
53
|
|
51
|
-
# Qt
|
54
|
+
# Qt Text Formats
|
52
55
|
|
53
56
|
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
|
54
57
|
QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
|
55
58
|
|
59
|
+
QtPropLineHeight = QTextBlockFormat.LineHeightTypes.ProportionalHeight
|
60
|
+
|
56
61
|
# Qt Painter Types
|
57
62
|
|
58
63
|
QtTransparent = QColor(0, 0, 0, 0)
|
@@ -65,6 +70,10 @@ QtPaintAnitAlias = QPainter.RenderHint.Antialiasing
|
|
65
70
|
QtMouseOver = QStyle.StateFlag.State_MouseOver
|
66
71
|
QtSelected = QStyle.StateFlag.State_Selected
|
67
72
|
|
73
|
+
# Qt Colour Types
|
74
|
+
|
75
|
+
QtHexRgb = QColor.NameFormat.HexRgb
|
76
|
+
|
68
77
|
# Qt Tree and Table Types
|
69
78
|
|
70
79
|
QtDecoration = Qt.ItemDataRole.DecorationRole
|
@@ -110,9 +119,16 @@ QtSizeIgnored = QSizePolicy.Policy.Ignored
|
|
110
119
|
QtSizeMinimum = QSizePolicy.Policy.Minimum
|
111
120
|
QtSizeMinimumExpanding = QSizePolicy.Policy.MinimumExpanding
|
112
121
|
|
113
|
-
#
|
122
|
+
# Resize Mode
|
123
|
+
|
124
|
+
QtHeaderStretch = QHeaderView.ResizeMode.Stretch
|
125
|
+
QtHeaderToContents = QHeaderView.ResizeMode.ResizeToContents
|
126
|
+
QtHeaderFixed = QHeaderView.ResizeMode.Fixed
|
127
|
+
|
128
|
+
# Scroll Bar Policy
|
114
129
|
|
115
|
-
|
130
|
+
QtScrollAlwaysOff = Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
131
|
+
QtScrollAsNeeded = Qt.ScrollBarPolicy.ScrollBarAsNeeded
|
116
132
|
|
117
133
|
# Maps
|
118
134
|
|
@@ -128,7 +144,7 @@ FONT_WEIGHTS: dict[int, int] = {
|
|
128
144
|
QFont.Weight.Black: 900,
|
129
145
|
}
|
130
146
|
|
131
|
-
FONT_STYLE: dict[
|
147
|
+
FONT_STYLE: dict[QFont.Style, str] = {
|
132
148
|
QFont.Style.StyleNormal: "normal",
|
133
149
|
QFont.Style.StyleItalic: "italic",
|
134
150
|
QFont.Style.StyleOblique: "oblique",
|
novelwriter/core/tohtml.py
DELETED
@@ -1,530 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
novelWriter – HTML Text Converter
|
3
|
-
=================================
|
4
|
-
|
5
|
-
File History:
|
6
|
-
Created: 2019-05-07 [0.0.1] ToHtml
|
7
|
-
|
8
|
-
This file is a part of novelWriter
|
9
|
-
Copyright 2018–2024, Veronica Berglyd Olsen
|
10
|
-
|
11
|
-
This program is free software: you can redistribute it and/or modify
|
12
|
-
it under the terms of the GNU General Public License as published by
|
13
|
-
the Free Software Foundation, either version 3 of the License, or
|
14
|
-
(at your option) any later version.
|
15
|
-
|
16
|
-
This program is distributed in the hope that it will be useful, but
|
17
|
-
WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
19
|
-
General Public License for more details.
|
20
|
-
|
21
|
-
You should have received a copy of the GNU General Public License
|
22
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
23
|
-
"""
|
24
|
-
from __future__ import annotations
|
25
|
-
|
26
|
-
import json
|
27
|
-
import logging
|
28
|
-
|
29
|
-
from pathlib import Path
|
30
|
-
from time import time
|
31
|
-
|
32
|
-
from novelwriter.common import formatTimeStamp
|
33
|
-
from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
|
34
|
-
from novelwriter.core.project import NWProject
|
35
|
-
from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
|
36
|
-
from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
|
37
|
-
|
38
|
-
logger = logging.getLogger(__name__)
|
39
|
-
|
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'>"),
|
51
|
-
}
|
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
|
-
|
69
|
-
|
70
|
-
class ToHtml(Tokenizer):
|
71
|
-
"""Core: HTML Document Writer
|
72
|
-
|
73
|
-
Extend the Tokenizer class to writer HTML output. This class is
|
74
|
-
also used by the Document Viewer, and Manuscript Build Preview.
|
75
|
-
"""
|
76
|
-
|
77
|
-
def __init__(self, project: NWProject) -> None:
|
78
|
-
super().__init__(project)
|
79
|
-
|
80
|
-
self._cssStyles = True
|
81
|
-
self._fullHTML: list[str] = []
|
82
|
-
|
83
|
-
# Internals
|
84
|
-
self._trMap = {}
|
85
|
-
self._usedNotes: dict[str, int] = {}
|
86
|
-
self.setReplaceUnicode(False)
|
87
|
-
|
88
|
-
return
|
89
|
-
|
90
|
-
##
|
91
|
-
# Properties
|
92
|
-
##
|
93
|
-
|
94
|
-
@property
|
95
|
-
def fullHTML(self) -> list[str]:
|
96
|
-
return self._fullHTML
|
97
|
-
|
98
|
-
##
|
99
|
-
# Setters
|
100
|
-
##
|
101
|
-
|
102
|
-
def setStyles(self, cssStyles: bool) -> None:
|
103
|
-
"""Enable or disable CSS styling. Some elements may still have
|
104
|
-
class tags.
|
105
|
-
"""
|
106
|
-
self._cssStyles = cssStyles
|
107
|
-
return
|
108
|
-
|
109
|
-
def setReplaceUnicode(self, doReplace: bool) -> None:
|
110
|
-
"""Set the translation map to either minimal or full unicode for
|
111
|
-
html entities replacement.
|
112
|
-
"""
|
113
|
-
# Control characters must always be replaced
|
114
|
-
# Angle brackets are replaced later as they are also used in
|
115
|
-
# formatting codes
|
116
|
-
self._trMap = str.maketrans({"&": "&"})
|
117
|
-
if doReplace:
|
118
|
-
# Extend to all relevant Unicode characters
|
119
|
-
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
|
120
|
-
return
|
121
|
-
|
122
|
-
##
|
123
|
-
# Class Methods
|
124
|
-
##
|
125
|
-
|
126
|
-
def getFullResultSize(self) -> int:
|
127
|
-
"""Return the size of the full HTML result."""
|
128
|
-
return sum(len(x) for x in self._fullHTML)
|
129
|
-
|
130
|
-
def doPreProcessing(self) -> None:
|
131
|
-
"""Extend the auto-replace to also properly encode some unicode
|
132
|
-
characters into their respective HTML entities.
|
133
|
-
"""
|
134
|
-
super().doPreProcessing()
|
135
|
-
self._text = self._text.translate(self._trMap)
|
136
|
-
return
|
137
|
-
|
138
|
-
def doConvert(self) -> None:
|
139
|
-
"""Convert the list of text tokens into an HTML document."""
|
140
|
-
self._result = ""
|
141
|
-
|
142
|
-
if self._isNovel:
|
143
|
-
# For story files, we bump the titles one level up
|
144
|
-
h1Cl = " class='title'"
|
145
|
-
h1 = "h1"
|
146
|
-
h2 = "h1"
|
147
|
-
h3 = "h2"
|
148
|
-
h4 = "h3"
|
149
|
-
else:
|
150
|
-
h1Cl = ""
|
151
|
-
h1 = "h1"
|
152
|
-
h2 = "h2"
|
153
|
-
h3 = "h3"
|
154
|
-
h4 = "h4"
|
155
|
-
|
156
|
-
lines = []
|
157
|
-
tHandle = self._handle
|
158
|
-
|
159
|
-
for tType, nHead, tText, tFormat, tStyle in self._tokens:
|
160
|
-
|
161
|
-
# Replace < and > with HTML entities
|
162
|
-
if tFormat:
|
163
|
-
# If we have formatting, we must recompute the locations
|
164
|
-
cText = []
|
165
|
-
i = 0
|
166
|
-
for c in tText:
|
167
|
-
if c == "<":
|
168
|
-
cText.append("<")
|
169
|
-
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
|
170
|
-
i += 4
|
171
|
-
elif c == ">":
|
172
|
-
cText.append(">")
|
173
|
-
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
|
174
|
-
i += 4
|
175
|
-
else:
|
176
|
-
cText.append(c)
|
177
|
-
i += 1
|
178
|
-
tText = "".join(cText)
|
179
|
-
else:
|
180
|
-
# If we don't have formatting, we can do a plain replace
|
181
|
-
tText = tText.replace("<", "<").replace(">", ">")
|
182
|
-
|
183
|
-
# Styles
|
184
|
-
aStyle = []
|
185
|
-
if tStyle is not None and self._cssStyles:
|
186
|
-
if tStyle & self.A_LEFT:
|
187
|
-
aStyle.append("text-align: left;")
|
188
|
-
elif tStyle & self.A_RIGHT:
|
189
|
-
aStyle.append("text-align: right;")
|
190
|
-
elif tStyle & self.A_CENTRE:
|
191
|
-
aStyle.append("text-align: center;")
|
192
|
-
elif tStyle & self.A_JUSTIFY:
|
193
|
-
aStyle.append("text-align: justify;")
|
194
|
-
|
195
|
-
if tStyle & self.A_PBB:
|
196
|
-
aStyle.append("page-break-before: always;")
|
197
|
-
if tStyle & self.A_PBA:
|
198
|
-
aStyle.append("page-break-after: always;")
|
199
|
-
|
200
|
-
if tStyle & self.A_Z_BTMMRG:
|
201
|
-
aStyle.append("margin-bottom: 0;")
|
202
|
-
if tStyle & self.A_Z_TOPMRG:
|
203
|
-
aStyle.append("margin-top: 0;")
|
204
|
-
|
205
|
-
if tStyle & self.A_IND_L:
|
206
|
-
aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
|
207
|
-
if tStyle & self.A_IND_R:
|
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;")
|
211
|
-
|
212
|
-
if aStyle:
|
213
|
-
stVals = " ".join(aStyle)
|
214
|
-
hStyle = f" style='{stVals}'"
|
215
|
-
else:
|
216
|
-
hStyle = ""
|
217
|
-
|
218
|
-
if self._linkHeadings and tHandle:
|
219
|
-
aNm = f"<a name='{tHandle}:T{nHead:04d}'></a>"
|
220
|
-
else:
|
221
|
-
aNm = ""
|
222
|
-
|
223
|
-
# Process Text Type
|
224
|
-
if tType == self.T_TEXT:
|
225
|
-
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
|
226
|
-
|
227
|
-
elif tType == self.T_TITLE:
|
228
|
-
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
229
|
-
lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
|
230
|
-
|
231
|
-
elif tType == self.T_HEAD1:
|
232
|
-
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
233
|
-
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
|
234
|
-
|
235
|
-
elif tType == self.T_HEAD2:
|
236
|
-
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
237
|
-
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
238
|
-
|
239
|
-
elif tType == self.T_HEAD3:
|
240
|
-
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
241
|
-
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
|
242
|
-
|
243
|
-
elif tType == self.T_HEAD4:
|
244
|
-
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
245
|
-
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
|
246
|
-
|
247
|
-
elif tType == self.T_SEP:
|
248
|
-
lines.append(f"<p class='sep'{hStyle}>{tText}</p>\n")
|
249
|
-
|
250
|
-
elif tType == self.T_SKIP:
|
251
|
-
lines.append(f"<p class='skip'{hStyle}> </p>\n")
|
252
|
-
|
253
|
-
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
254
|
-
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
|
255
|
-
|
256
|
-
elif tType == self.T_SHORT and self._doSynopsis:
|
257
|
-
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
|
258
|
-
|
259
|
-
elif tType == self.T_COMMENT and self._doComments:
|
260
|
-
lines.append(self._formatComments(self._formatText(tText, tFormat)))
|
261
|
-
|
262
|
-
elif tType == self.T_KEYWORD and self._doKeywords:
|
263
|
-
tag, text = self._formatKeywords(tText)
|
264
|
-
kClass = f" class='meta meta-{tag}'" if tag else ""
|
265
|
-
tTemp = f"<p{kClass}{hStyle}>{text}</p>\n"
|
266
|
-
lines.append(tTemp)
|
267
|
-
|
268
|
-
self._result = "".join(lines)
|
269
|
-
self._fullHTML.append(self._result)
|
270
|
-
|
271
|
-
return
|
272
|
-
|
273
|
-
def appendFootnotes(self) -> None:
|
274
|
-
"""Append the footnotes in the buffer."""
|
275
|
-
if self._usedNotes:
|
276
|
-
footnotes = self._localLookup("Footnotes")
|
277
|
-
|
278
|
-
lines = []
|
279
|
-
lines.append(f"<h3>{footnotes}</h3>\n")
|
280
|
-
lines.append("<ol>\n")
|
281
|
-
for key, index in self._usedNotes.items():
|
282
|
-
if content := self._footnotes.get(key):
|
283
|
-
text = self._formatText(*content)
|
284
|
-
lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
|
285
|
-
lines.append("</ol>\n")
|
286
|
-
|
287
|
-
result = "".join(lines)
|
288
|
-
self._result += result
|
289
|
-
self._fullHTML.append(result)
|
290
|
-
|
291
|
-
return
|
292
|
-
|
293
|
-
def saveHtml5(self, path: str | Path) -> None:
|
294
|
-
"""Save the data to an HTML file."""
|
295
|
-
with open(path, mode="w", encoding="utf-8") as fObj:
|
296
|
-
fObj.write((
|
297
|
-
"<!DOCTYPE html>\n"
|
298
|
-
"<html>\n"
|
299
|
-
"<head>\n"
|
300
|
-
"<meta charset='utf-8'>\n"
|
301
|
-
"<title>{title:s}</title>\n"
|
302
|
-
"<style>\n"
|
303
|
-
"{style:s}\n"
|
304
|
-
"</style>\n"
|
305
|
-
"</head>\n"
|
306
|
-
"<body>\n"
|
307
|
-
"<article>\n"
|
308
|
-
"{body:s}\n"
|
309
|
-
"</article>\n"
|
310
|
-
"</body>\n"
|
311
|
-
"</html>\n"
|
312
|
-
).format(
|
313
|
-
title=self._project.data.name,
|
314
|
-
style="\n".join(self.getStyleSheet()),
|
315
|
-
body=("".join(self._fullHTML)).replace("\t", "	").rstrip(),
|
316
|
-
))
|
317
|
-
logger.info("Wrote file: %s", path)
|
318
|
-
return
|
319
|
-
|
320
|
-
def saveHtmlJson(self, path: str | Path) -> None:
|
321
|
-
"""Save the data to a JSON file."""
|
322
|
-
timeStamp = time()
|
323
|
-
data = {
|
324
|
-
"meta": {
|
325
|
-
"projectName": self._project.data.name,
|
326
|
-
"novelAuthor": self._project.data.author,
|
327
|
-
"buildTime": int(timeStamp),
|
328
|
-
"buildTimeStr": formatTimeStamp(timeStamp),
|
329
|
-
},
|
330
|
-
"text": {
|
331
|
-
"css": self.getStyleSheet(),
|
332
|
-
"html": [t.replace("\t", "	").rstrip().split("\n") for t in self.fullHTML],
|
333
|
-
}
|
334
|
-
}
|
335
|
-
with open(path, mode="w", encoding="utf-8") as fObj:
|
336
|
-
json.dump(data, fObj, indent=2)
|
337
|
-
logger.info("Wrote file: %s", path)
|
338
|
-
return
|
339
|
-
|
340
|
-
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
|
341
|
-
"""Replace tabs with spaces in the html."""
|
342
|
-
htmlText = []
|
343
|
-
tabSpace = spaceChar*nSpaces
|
344
|
-
for aLine in self._fullHTML:
|
345
|
-
htmlText.append(aLine.replace("\t", tabSpace))
|
346
|
-
|
347
|
-
self._fullHTML = htmlText
|
348
|
-
return
|
349
|
-
|
350
|
-
def getStyleSheet(self) -> list[str]:
|
351
|
-
"""Generate a stylesheet for the current settings."""
|
352
|
-
if not self._cssStyles:
|
353
|
-
return []
|
354
|
-
|
355
|
-
mScale = self._lineHeight/1.15
|
356
|
-
|
357
|
-
styles = []
|
358
|
-
font = self._textFont
|
359
|
-
styles.append((
|
360
|
-
"body {{"
|
361
|
-
"font-family: '{0:s}'; font-size: {1:d}pt; "
|
362
|
-
"font-weight: {2:d}; font-style: {3:s};"
|
363
|
-
"}}"
|
364
|
-
).format(
|
365
|
-
font.family(), font.pointSize(),
|
366
|
-
FONT_WEIGHTS.get(font.weight(), 400),
|
367
|
-
FONT_STYLE.get(font.style(), "normal"),
|
368
|
-
))
|
369
|
-
styles.append((
|
370
|
-
"p {{"
|
371
|
-
"text-align: {0}; line-height: {1:d}%; "
|
372
|
-
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
|
373
|
-
"}}"
|
374
|
-
).format(
|
375
|
-
"justify" if self._doJustify else "left",
|
376
|
-
round(100 * self._lineHeight),
|
377
|
-
mScale * self._marginText[0],
|
378
|
-
mScale * self._marginText[1],
|
379
|
-
))
|
380
|
-
styles.append((
|
381
|
-
"h1 {{"
|
382
|
-
"color: rgb(66, 113, 174); "
|
383
|
-
"page-break-after: avoid; "
|
384
|
-
"margin-top: {0:.2f}em; "
|
385
|
-
"margin-bottom: {1:.2f}em;"
|
386
|
-
"}}"
|
387
|
-
).format(
|
388
|
-
mScale * self._marginHead1[0], mScale * self._marginHead1[1]
|
389
|
-
))
|
390
|
-
styles.append((
|
391
|
-
"h2 {{"
|
392
|
-
"color: rgb(66, 113, 174); "
|
393
|
-
"page-break-after: avoid; "
|
394
|
-
"margin-top: {0:.2f}em; "
|
395
|
-
"margin-bottom: {1:.2f}em;"
|
396
|
-
"}}"
|
397
|
-
).format(
|
398
|
-
mScale * self._marginHead2[0], mScale * self._marginHead2[1]
|
399
|
-
))
|
400
|
-
styles.append((
|
401
|
-
"h3 {{"
|
402
|
-
"color: rgb(50, 50, 50); "
|
403
|
-
"page-break-after: avoid; "
|
404
|
-
"margin-top: {0:.2f}em; "
|
405
|
-
"margin-bottom: {1:.2f}em;"
|
406
|
-
"}}"
|
407
|
-
).format(
|
408
|
-
mScale * self._marginHead3[0], mScale * self._marginHead3[1]
|
409
|
-
))
|
410
|
-
styles.append((
|
411
|
-
"h4 {{"
|
412
|
-
"color: rgb(50, 50, 50); "
|
413
|
-
"page-break-after: avoid; "
|
414
|
-
"margin-top: {0:.2f}em; "
|
415
|
-
"margin-bottom: {1:.2f}em;"
|
416
|
-
"}}"
|
417
|
-
).format(
|
418
|
-
mScale * self._marginHead4[0], mScale * self._marginHead4[1]
|
419
|
-
))
|
420
|
-
styles.append((
|
421
|
-
".title {{"
|
422
|
-
"font-size: 2.5em; "
|
423
|
-
"margin-top: {0:.2f}em; "
|
424
|
-
"margin-bottom: {1:.2f}em;"
|
425
|
-
"}}"
|
426
|
-
).format(
|
427
|
-
mScale * self._marginTitle[0], mScale * self._marginTitle[1]
|
428
|
-
))
|
429
|
-
styles.append((
|
430
|
-
".sep, .skip {{"
|
431
|
-
"text-align: center; "
|
432
|
-
"margin-top: {0:.2f}em; "
|
433
|
-
"margin-bottom: {1:.2f}em;"
|
434
|
-
"}}"
|
435
|
-
).format(
|
436
|
-
mScale, mScale
|
437
|
-
))
|
438
|
-
|
439
|
-
styles.append("a {color: rgb(66, 113, 174);}")
|
440
|
-
styles.append("mark {background: rgb(255, 255, 166);}")
|
441
|
-
styles.append(".keyword {color: rgb(245, 135, 31); font-weight: bold;}")
|
442
|
-
styles.append(".break {text-align: left;}")
|
443
|
-
styles.append(".synopsis {font-style: italic;}")
|
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);}")
|
447
|
-
|
448
|
-
return styles
|
449
|
-
|
450
|
-
##
|
451
|
-
# Internal Functions
|
452
|
-
##
|
453
|
-
|
454
|
-
def _formatText(self, text: str, tFmt: T_Formats) -> str:
|
455
|
-
"""Apply formatting tags to text."""
|
456
|
-
temp = text
|
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:
|
473
|
-
if data in self._footnotes:
|
474
|
-
index = len(self._usedNotes) + 1
|
475
|
-
self._usedNotes[data] = index
|
476
|
-
tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
|
477
|
-
else:
|
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
|
495
|
-
temp = temp.replace("\n", "<br>")
|
496
|
-
|
497
|
-
return stripEscape(temp)
|
498
|
-
|
499
|
-
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
|
500
|
-
"""Apply HTML formatting to synopsis."""
|
501
|
-
if synopsis:
|
502
|
-
sSynop = self._localLookup("Synopsis")
|
503
|
-
else:
|
504
|
-
sSynop = self._localLookup("Short Description")
|
505
|
-
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
|
506
|
-
|
507
|
-
def _formatComments(self, text: str) -> str:
|
508
|
-
"""Apply HTML formatting to comments."""
|
509
|
-
sComm = self._localLookup("Comment")
|
510
|
-
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
|
511
|
-
|
512
|
-
def _formatKeywords(self, text: str) -> tuple[str, str]:
|
513
|
-
"""Apply HTML formatting to keywords."""
|
514
|
-
valid, bits, _ = self._project.index.scanThis("@"+text)
|
515
|
-
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
|
516
|
-
return "", ""
|
517
|
-
|
518
|
-
result = f"<span class='keyword'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
519
|
-
if len(bits) > 1:
|
520
|
-
if bits[0] == nwKeyWords.TAG_KEY:
|
521
|
-
one, two = self._project.index.parseValue(bits[1])
|
522
|
-
result += f"<a class='tag' name='tag_{one}'>{one}</a>"
|
523
|
-
if two:
|
524
|
-
result += f" | <span class='optional'>{two}</a>"
|
525
|
-
else:
|
526
|
-
result += ", ".join(
|
527
|
-
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
|
528
|
-
)
|
529
|
-
|
530
|
-
return bits[0][1:], result
|