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.
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +3 -3
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +1 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +63 -0
- novelwriter/config.py +10 -3
- novelwriter/constants.py +153 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +34 -22
- novelwriter/core/docbuild.py +130 -169
- novelwriter/core/index.py +29 -18
- novelwriter/core/item.py +2 -2
- novelwriter/core/options.py +4 -1
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/dialogs/preferences.py +45 -32
- novelwriter/dialogs/projectsettings.py +3 -3
- novelwriter/enum.py +29 -23
- novelwriter/extensions/configlayout.py +24 -11
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1195 -0
- novelwriter/formats/tohtml.py +452 -0
- novelwriter/{core → formats}/tokenizer.py +483 -485
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +270 -320
- novelwriter/formats/toqdoc.py +436 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +240 -193
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +56 -30
- novelwriter/gui/docviewerpanel.py +3 -3
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +8 -4
- novelwriter/gui/mainmenu.py +121 -60
- novelwriter/gui/noveltree.py +35 -37
- novelwriter/gui/outline.py +186 -238
- novelwriter/gui/projtree.py +142 -131
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +5 -4
- novelwriter/guimain.py +43 -155
- novelwriter/shared.py +14 -4
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +155 -59
- novelwriter/tools/manusbuild.py +1 -1
- novelwriter/tools/manuscript.py +121 -78
- novelwriter/tools/manussettings.py +403 -260
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +16 -6
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,436 @@
|
|
1
|
+
"""
|
2
|
+
novelWriter – QTextDocument Converter
|
3
|
+
=====================================
|
4
|
+
|
5
|
+
File History:
|
6
|
+
Created: 2024-05-21 [2.5b1] ToQTextDocument
|
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 logging
|
27
|
+
|
28
|
+
from pathlib import Path
|
29
|
+
|
30
|
+
from PyQt5.QtCore import QMarginsF, QSizeF
|
31
|
+
from PyQt5.QtGui import (
|
32
|
+
QColor, QFont, QFontMetricsF, QPageSize, QTextBlockFormat, QTextCharFormat,
|
33
|
+
QTextCursor, QTextDocument
|
34
|
+
)
|
35
|
+
from PyQt5.QtPrintSupport import QPrinter
|
36
|
+
|
37
|
+
from novelwriter.constants import nwStyles, nwUnicode
|
38
|
+
from novelwriter.core.project import NWProject
|
39
|
+
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
|
40
|
+
from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
|
41
|
+
from novelwriter.types import (
|
42
|
+
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
43
|
+
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore,
|
44
|
+
QtPropLineHeight, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
45
|
+
)
|
46
|
+
|
47
|
+
logger = logging.getLogger(__name__)
|
48
|
+
|
49
|
+
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
|
50
|
+
|
51
|
+
|
52
|
+
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
|
53
|
+
if cursor.position() > 0:
|
54
|
+
cursor.insertBlock(bFmt)
|
55
|
+
else:
|
56
|
+
cursor.setBlockFormat(bFmt)
|
57
|
+
|
58
|
+
|
59
|
+
class ToQTextDocument(Tokenizer):
|
60
|
+
"""Core: QTextDocument Writer
|
61
|
+
|
62
|
+
Extend the Tokenizer class to generate a QTextDocument output. This
|
63
|
+
is intended for usage in the document viewer and build tool preview.
|
64
|
+
"""
|
65
|
+
|
66
|
+
def __init__(self, project: NWProject) -> None:
|
67
|
+
super().__init__(project)
|
68
|
+
self._document = QTextDocument()
|
69
|
+
self._document.setUndoRedoEnabled(False)
|
70
|
+
self._document.setDocumentMargin(0)
|
71
|
+
|
72
|
+
self._usedNotes: dict[str, int] = {}
|
73
|
+
self._usedFields: list[tuple[int, str]] = []
|
74
|
+
|
75
|
+
self._init = False
|
76
|
+
self._bold = QFont.Weight.Bold
|
77
|
+
self._normal = QFont.Weight.Normal
|
78
|
+
self._newPage = False
|
79
|
+
|
80
|
+
self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
|
81
|
+
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
|
82
|
+
|
83
|
+
return
|
84
|
+
|
85
|
+
##
|
86
|
+
# Properties
|
87
|
+
##
|
88
|
+
|
89
|
+
@property
|
90
|
+
def document(self) -> QTextDocument:
|
91
|
+
"""Return the document."""
|
92
|
+
return self._document
|
93
|
+
|
94
|
+
##
|
95
|
+
# Setters
|
96
|
+
##
|
97
|
+
|
98
|
+
def setPageLayout(
|
99
|
+
self, width: float, height: float, top: float, bottom: float, left: float, right: float
|
100
|
+
) -> None:
|
101
|
+
"""Set the document page size and margins in millimetres."""
|
102
|
+
self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
|
103
|
+
self._pageMargins = QMarginsF(left, top, right, bottom)
|
104
|
+
return
|
105
|
+
|
106
|
+
def setShowNewPage(self, state: bool) -> None:
|
107
|
+
"""Add markers for page breaks."""
|
108
|
+
self._newPage = state
|
109
|
+
return
|
110
|
+
|
111
|
+
##
|
112
|
+
# Class Methods
|
113
|
+
##
|
114
|
+
|
115
|
+
def initDocument(self) -> None:
|
116
|
+
"""Initialise all computed values of the document."""
|
117
|
+
super().initDocument()
|
118
|
+
|
119
|
+
self._document.setUndoRedoEnabled(False)
|
120
|
+
self._document.blockSignals(True)
|
121
|
+
self._document.clear()
|
122
|
+
self._document.setDefaultFont(self._textFont)
|
123
|
+
|
124
|
+
qMetric = QFontMetricsF(self._textFont)
|
125
|
+
mPx = qMetric.ascent() # 1 em in pixels
|
126
|
+
fPt = self._textFont.pointSizeF()
|
127
|
+
|
128
|
+
# Scaled Sizes
|
129
|
+
# ============
|
130
|
+
|
131
|
+
self._mHead = {
|
132
|
+
BlockTyp.TITLE: (mPx * self._marginTitle[0], mPx * self._marginTitle[1]),
|
133
|
+
BlockTyp.HEAD1: (mPx * self._marginHead1[0], mPx * self._marginHead1[1]),
|
134
|
+
BlockTyp.HEAD2: (mPx * self._marginHead2[0], mPx * self._marginHead2[1]),
|
135
|
+
BlockTyp.HEAD3: (mPx * self._marginHead3[0], mPx * self._marginHead3[1]),
|
136
|
+
BlockTyp.HEAD4: (mPx * self._marginHead4[0], mPx * self._marginHead4[1]),
|
137
|
+
}
|
138
|
+
|
139
|
+
hScale = self._scaleHeads
|
140
|
+
self._sHead = {
|
141
|
+
BlockTyp.TITLE: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
|
142
|
+
BlockTyp.HEAD1: (nwStyles.H_SIZES.get(1, 1.0) * fPt) if hScale else fPt,
|
143
|
+
BlockTyp.HEAD2: (nwStyles.H_SIZES.get(2, 1.0) * fPt) if hScale else fPt,
|
144
|
+
BlockTyp.HEAD3: (nwStyles.H_SIZES.get(3, 1.0) * fPt) if hScale else fPt,
|
145
|
+
BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt,
|
146
|
+
}
|
147
|
+
|
148
|
+
self._mText = (mPx * self._marginText[0], mPx * self._marginText[1])
|
149
|
+
self._mMeta = (mPx * self._marginMeta[0], mPx * self._marginMeta[1])
|
150
|
+
self._mSep = (mPx * self._marginSep[0], mPx * self._marginSep[1])
|
151
|
+
|
152
|
+
self._mIndent = mPx * 2.0
|
153
|
+
self._tIndent = mPx * self._firstWidth
|
154
|
+
|
155
|
+
# Text Formats
|
156
|
+
# ============
|
157
|
+
|
158
|
+
self._blockFmt = QTextBlockFormat()
|
159
|
+
self._blockFmt.setTopMargin(self._mText[0])
|
160
|
+
self._blockFmt.setBottomMargin(self._mText[1])
|
161
|
+
self._blockFmt.setAlignment(QtAlignAbsolute)
|
162
|
+
self._blockFmt.setLineHeight(100.0*self._lineHeight, QtPropLineHeight)
|
163
|
+
|
164
|
+
self._charFmt = QTextCharFormat()
|
165
|
+
self._charFmt.setBackground(QtTransparent)
|
166
|
+
self._charFmt.setForeground(self._theme.text)
|
167
|
+
|
168
|
+
self._init = True
|
169
|
+
|
170
|
+
return
|
171
|
+
|
172
|
+
def doConvert(self) -> None:
|
173
|
+
"""Write text tokens into the document."""
|
174
|
+
if not self._init:
|
175
|
+
return
|
176
|
+
|
177
|
+
self._document.blockSignals(True)
|
178
|
+
cursor = QTextCursor(self._document)
|
179
|
+
cursor.movePosition(QTextCursor.MoveOperation.End)
|
180
|
+
|
181
|
+
for tType, tMeta, tText, tFormat, tStyle in self._blocks:
|
182
|
+
|
183
|
+
bFmt = QTextBlockFormat(self._blockFmt)
|
184
|
+
if tType in (BlockTyp.COMMENT, BlockTyp.KEYWORD):
|
185
|
+
bFmt.setTopMargin(self._mMeta[0])
|
186
|
+
bFmt.setBottomMargin(self._mMeta[1])
|
187
|
+
elif tType == BlockTyp.SEP:
|
188
|
+
bFmt.setTopMargin(self._mSep[0])
|
189
|
+
bFmt.setBottomMargin(self._mSep[1])
|
190
|
+
|
191
|
+
if tStyle & BlockFmt.LEFT:
|
192
|
+
bFmt.setAlignment(QtAlignLeft)
|
193
|
+
elif tStyle & BlockFmt.RIGHT:
|
194
|
+
bFmt.setAlignment(QtAlignRight)
|
195
|
+
elif tStyle & BlockFmt.CENTRE:
|
196
|
+
bFmt.setAlignment(QtAlignCenter)
|
197
|
+
elif tStyle & BlockFmt.JUSTIFY:
|
198
|
+
bFmt.setAlignment(QtAlignJustify)
|
199
|
+
|
200
|
+
if tStyle & BlockFmt.PBB:
|
201
|
+
self._insertNewPageMarker(cursor)
|
202
|
+
bFmt.setPageBreakPolicy(QtPageBreakBefore)
|
203
|
+
if tStyle & BlockFmt.PBA:
|
204
|
+
bFmt.setPageBreakPolicy(QtPageBreakAfter)
|
205
|
+
|
206
|
+
if tStyle & BlockFmt.Z_BTM:
|
207
|
+
bFmt.setBottomMargin(0.0)
|
208
|
+
if tStyle & BlockFmt.Z_TOP:
|
209
|
+
bFmt.setTopMargin(0.0)
|
210
|
+
|
211
|
+
if tStyle & BlockFmt.IND_L:
|
212
|
+
bFmt.setLeftMargin(self._mIndent)
|
213
|
+
if tStyle & BlockFmt.IND_R:
|
214
|
+
bFmt.setRightMargin(self._mIndent)
|
215
|
+
if tStyle & BlockFmt.IND_T:
|
216
|
+
bFmt.setTextIndent(self._tIndent)
|
217
|
+
|
218
|
+
if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD):
|
219
|
+
newBlock(cursor, bFmt)
|
220
|
+
self._insertFragments(tText, tFormat, cursor, self._charFmt)
|
221
|
+
|
222
|
+
elif tType in HEADINGS:
|
223
|
+
bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
|
224
|
+
newBlock(cursor, bFmt)
|
225
|
+
cursor.insertText(tText, cFmt)
|
226
|
+
|
227
|
+
elif tType == BlockTyp.SEP:
|
228
|
+
newBlock(cursor, bFmt)
|
229
|
+
cursor.insertText(tText, self._charFmt)
|
230
|
+
|
231
|
+
elif tType == BlockTyp.SKIP:
|
232
|
+
newBlock(cursor, bFmt)
|
233
|
+
cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
|
234
|
+
|
235
|
+
if tStyle & BlockFmt.PBA:
|
236
|
+
self._insertNewPageMarker(cursor)
|
237
|
+
|
238
|
+
self._document.blockSignals(False)
|
239
|
+
|
240
|
+
return
|
241
|
+
|
242
|
+
def saveDocument(self, path: Path) -> None:
|
243
|
+
"""Save the document as a PDF file."""
|
244
|
+
m = self._pageMargins
|
245
|
+
|
246
|
+
printer = QPrinter(QPrinter.PrinterMode.PrinterResolution)
|
247
|
+
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
|
248
|
+
printer.setPageSize(self._pageSize)
|
249
|
+
printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter)
|
250
|
+
printer.setOutputFileName(str(path))
|
251
|
+
|
252
|
+
self._document.setPageSize(self._pageSize.size(QPageSize.Unit.Point))
|
253
|
+
self._document.print(printer)
|
254
|
+
|
255
|
+
return
|
256
|
+
|
257
|
+
def closeDocument(self) -> None:
|
258
|
+
"""Run close document tasks."""
|
259
|
+
self._document.blockSignals(True)
|
260
|
+
|
261
|
+
# Replace fields if there are stats available
|
262
|
+
if self._usedFields and self._counts:
|
263
|
+
cursor = QTextCursor(self._document)
|
264
|
+
for pos, field in reversed(self._usedFields):
|
265
|
+
if (value := self._counts.get(field)) is not None:
|
266
|
+
cursor.setPosition(pos, QtMoveAnchor)
|
267
|
+
cursor.setPosition(pos + 1, QtKeepAnchor)
|
268
|
+
cursor.insertText(self._formatInt(value))
|
269
|
+
|
270
|
+
# Add footnotes
|
271
|
+
if self._usedNotes:
|
272
|
+
cursor = QTextCursor(self._document)
|
273
|
+
cursor.movePosition(QTextCursor.MoveOperation.End)
|
274
|
+
|
275
|
+
bFmt, cFmt = self._genHeadStyle(BlockTyp.HEAD4, "", self._blockFmt)
|
276
|
+
newBlock(cursor, bFmt)
|
277
|
+
cursor.insertText(self._localLookup("Footnotes"), cFmt)
|
278
|
+
|
279
|
+
for key, index in self._usedNotes.items():
|
280
|
+
if content := self._footnotes.get(key):
|
281
|
+
cFmt = QTextCharFormat(self._charFmt)
|
282
|
+
cFmt.setForeground(self._theme.code)
|
283
|
+
cFmt.setAnchor(True)
|
284
|
+
cFmt.setAnchorNames([f"footnote_{index}"])
|
285
|
+
newBlock(cursor, self._blockFmt)
|
286
|
+
cursor.insertText(f"{index}. ", cFmt)
|
287
|
+
self._insertFragments(*content, cursor, self._charFmt)
|
288
|
+
|
289
|
+
self._document.blockSignals(False)
|
290
|
+
|
291
|
+
return
|
292
|
+
|
293
|
+
##
|
294
|
+
# Internal Functions
|
295
|
+
##
|
296
|
+
|
297
|
+
def _insertFragments(
|
298
|
+
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
|
299
|
+
) -> None:
|
300
|
+
"""Apply formatting tags to text."""
|
301
|
+
cFmt = QTextCharFormat(dFmt)
|
302
|
+
temp = text.replace("\n", nwUnicode.U_LSEP)
|
303
|
+
start = 0
|
304
|
+
primary: QColor | None = None
|
305
|
+
for pos, fmt, data in tFmt:
|
306
|
+
|
307
|
+
# Insert buffer with previous format
|
308
|
+
cursor.insertText(temp[start:pos], cFmt)
|
309
|
+
|
310
|
+
# Construct next format
|
311
|
+
if fmt == TextFmt.B_B:
|
312
|
+
cFmt.setFontWeight(self._bold)
|
313
|
+
elif fmt == TextFmt.B_E:
|
314
|
+
cFmt.setFontWeight(self._normal)
|
315
|
+
elif fmt == TextFmt.I_B:
|
316
|
+
cFmt.setFontItalic(True)
|
317
|
+
elif fmt == TextFmt.I_E:
|
318
|
+
cFmt.setFontItalic(False)
|
319
|
+
elif fmt == TextFmt.D_B:
|
320
|
+
cFmt.setFontStrikeOut(True)
|
321
|
+
elif fmt == TextFmt.D_E:
|
322
|
+
cFmt.setFontStrikeOut(False)
|
323
|
+
elif fmt == TextFmt.U_B:
|
324
|
+
cFmt.setFontUnderline(True)
|
325
|
+
elif fmt == TextFmt.U_E:
|
326
|
+
cFmt.setFontUnderline(False)
|
327
|
+
elif fmt == TextFmt.M_B:
|
328
|
+
cFmt.setBackground(self._theme.highlight)
|
329
|
+
elif fmt == TextFmt.M_E:
|
330
|
+
cFmt.setBackground(QtTransparent)
|
331
|
+
elif fmt == TextFmt.SUP_B:
|
332
|
+
cFmt.setVerticalAlignment(QtVAlignSuper)
|
333
|
+
elif fmt == TextFmt.SUP_E:
|
334
|
+
cFmt.setVerticalAlignment(QtVAlignNormal)
|
335
|
+
elif fmt == TextFmt.SUB_B:
|
336
|
+
cFmt.setVerticalAlignment(QtVAlignSub)
|
337
|
+
elif fmt == TextFmt.SUB_E:
|
338
|
+
cFmt.setVerticalAlignment(QtVAlignNormal)
|
339
|
+
elif fmt == TextFmt.COL_B:
|
340
|
+
if color := self._classes.get(data):
|
341
|
+
cFmt.setForeground(color)
|
342
|
+
primary = color
|
343
|
+
elif fmt == TextFmt.COL_E:
|
344
|
+
cFmt.setForeground(self._theme.text)
|
345
|
+
primary = None
|
346
|
+
elif fmt == TextFmt.ANM_B:
|
347
|
+
cFmt.setAnchor(True)
|
348
|
+
cFmt.setAnchorNames([data])
|
349
|
+
elif fmt == TextFmt.ANM_E:
|
350
|
+
cFmt.setAnchor(False)
|
351
|
+
elif fmt == TextFmt.ARF_B:
|
352
|
+
cFmt.setFontUnderline(True)
|
353
|
+
cFmt.setAnchor(True)
|
354
|
+
cFmt.setAnchorHref(data)
|
355
|
+
elif fmt == TextFmt.ARF_E:
|
356
|
+
cFmt.setFontUnderline(False)
|
357
|
+
cFmt.setAnchor(False)
|
358
|
+
cFmt.setAnchorHref("")
|
359
|
+
elif fmt == TextFmt.HRF_B:
|
360
|
+
cFmt.setForeground(self._theme.link)
|
361
|
+
cFmt.setFontUnderline(True)
|
362
|
+
cFmt.setAnchor(True)
|
363
|
+
cFmt.setAnchorHref(data)
|
364
|
+
elif fmt == TextFmt.HRF_E:
|
365
|
+
cFmt.setForeground(primary or self._theme.text)
|
366
|
+
cFmt.setFontUnderline(False)
|
367
|
+
cFmt.setAnchor(False)
|
368
|
+
cFmt.setAnchorHref("")
|
369
|
+
elif fmt == TextFmt.FNOTE:
|
370
|
+
xFmt = QTextCharFormat(self._charFmt)
|
371
|
+
xFmt.setForeground(self._theme.code)
|
372
|
+
xFmt.setVerticalAlignment(QtVAlignSuper)
|
373
|
+
if data in self._footnotes:
|
374
|
+
index = len(self._usedNotes) + 1
|
375
|
+
self._usedNotes[data] = index
|
376
|
+
xFmt.setAnchor(True)
|
377
|
+
xFmt.setAnchorHref(f"#footnote_{index}")
|
378
|
+
xFmt.setFontUnderline(True)
|
379
|
+
cursor.insertText(f"[{index}]", xFmt)
|
380
|
+
else:
|
381
|
+
cursor.insertText("[ERR]", cFmt)
|
382
|
+
elif fmt == TextFmt.FIELD:
|
383
|
+
if field := data.partition(":")[2]:
|
384
|
+
self._usedFields.append((cursor.position(), field))
|
385
|
+
cursor.insertText("0", cFmt)
|
386
|
+
pass
|
387
|
+
|
388
|
+
# Move pos for next pass
|
389
|
+
start = pos
|
390
|
+
|
391
|
+
# Insert whatever is left in the buffer
|
392
|
+
cursor.insertText(temp[start:], cFmt)
|
393
|
+
|
394
|
+
return
|
395
|
+
|
396
|
+
def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
|
397
|
+
"""Insert a new page marker."""
|
398
|
+
if self._newPage:
|
399
|
+
cursor.insertHtml("<hr width='100%'>")
|
400
|
+
|
401
|
+
hFmt = cursor.blockFormat()
|
402
|
+
hFmt.setBottomMargin(0.0)
|
403
|
+
hFmt.setLineHeight(75.0, QtPropLineHeight)
|
404
|
+
cursor.setBlockFormat(hFmt)
|
405
|
+
|
406
|
+
bFmt = QTextBlockFormat(self._blockFmt)
|
407
|
+
bFmt.setAlignment(QtAlignCenter)
|
408
|
+
bFmt.setTopMargin(0.0)
|
409
|
+
bFmt.setLineHeight(75.0, QtPropLineHeight)
|
410
|
+
|
411
|
+
cFmt = QTextCharFormat(self._charFmt)
|
412
|
+
cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
|
413
|
+
cFmt.setForeground(self._theme.comment)
|
414
|
+
|
415
|
+
newBlock(cursor, bFmt)
|
416
|
+
cursor.insertText(self._project.localLookup("New Page"), cFmt)
|
417
|
+
return
|
418
|
+
|
419
|
+
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
|
420
|
+
"""Generate a heading style set."""
|
421
|
+
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
|
422
|
+
|
423
|
+
bFmt = QTextBlockFormat(rFmt)
|
424
|
+
bFmt.setTopMargin(mTop)
|
425
|
+
bFmt.setBottomMargin(mBottom)
|
426
|
+
|
427
|
+
hCol = self._colorHeads and hType != BlockTyp.TITLE
|
428
|
+
cFmt = QTextCharFormat(self._charFmt)
|
429
|
+
cFmt.setForeground(self._theme.head if hCol else self._theme.text)
|
430
|
+
cFmt.setFontWeight(self._bold if self._boldHeads else self._normal)
|
431
|
+
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
|
432
|
+
if hKey:
|
433
|
+
cFmt.setAnchorNames([hKey])
|
434
|
+
cFmt.setAnchor(True)
|
435
|
+
|
436
|
+
return bFmt, cFmt
|
@@ -0,0 +1,91 @@
|
|
1
|
+
"""
|
2
|
+
novelWriter – Raw NW Text Format
|
3
|
+
================================
|
4
|
+
|
5
|
+
File History:
|
6
|
+
Created: 2024-10-15 [2.6b1] ToRaw
|
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.core.project import NWProject
|
34
|
+
from novelwriter.formats.tokenizer import Tokenizer
|
35
|
+
|
36
|
+
logger = logging.getLogger(__name__)
|
37
|
+
|
38
|
+
|
39
|
+
class ToRaw(Tokenizer):
|
40
|
+
"""Core: Raw novelWriter Text Writer
|
41
|
+
|
42
|
+
A class that will collect the minimally altered original source text
|
43
|
+
and write it to either a text or JSON file.
|
44
|
+
"""
|
45
|
+
|
46
|
+
def __init__(self, project: NWProject) -> None:
|
47
|
+
super().__init__(project)
|
48
|
+
self._keepRaw = True
|
49
|
+
self._noTokens = True
|
50
|
+
return
|
51
|
+
|
52
|
+
def doConvert(self) -> None:
|
53
|
+
"""No conversion to perform."""
|
54
|
+
return
|
55
|
+
|
56
|
+
def closeDocument(self) -> None:
|
57
|
+
"""Nothing to close."""
|
58
|
+
return
|
59
|
+
|
60
|
+
def saveDocument(self, path: Path) -> None:
|
61
|
+
"""Save the raw text to a plain text file."""
|
62
|
+
if path.suffix.lower() == ".json":
|
63
|
+
ts = time()
|
64
|
+
data = {
|
65
|
+
"meta": {
|
66
|
+
"projectName": self._project.data.name,
|
67
|
+
"novelAuthor": self._project.data.author,
|
68
|
+
"buildTime": int(ts),
|
69
|
+
"buildTimeStr": formatTimeStamp(ts),
|
70
|
+
},
|
71
|
+
"text": {
|
72
|
+
"nwd": [page.rstrip("\n").split("\n") for page in self._raw],
|
73
|
+
}
|
74
|
+
}
|
75
|
+
with open(path, mode="w", encoding="utf-8") as fObj:
|
76
|
+
json.dump(data, fObj, indent=2)
|
77
|
+
|
78
|
+
else:
|
79
|
+
with open(path, mode="w", encoding="utf-8") as outFile:
|
80
|
+
for nwdPage in self._raw:
|
81
|
+
outFile.write(nwdPage)
|
82
|
+
|
83
|
+
logger.info("Wrote file: %s", path)
|
84
|
+
|
85
|
+
return
|
86
|
+
|
87
|
+
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
|
88
|
+
"""Replace tabs with spaces."""
|
89
|
+
spaces = spaceChar*nSpaces
|
90
|
+
self._raw = [p.replace("\t", spaces) for p in self._raw]
|
91
|
+
return
|