novelWriter 2.5.3__py3-none-any.whl → 2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/METADATA +2 -2
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/RECORD +123 -103
  3. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +50 -11
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  16. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  17. novelwriter/assets/i18n/project_de_DE.json +2 -0
  18. novelwriter/assets/i18n/project_en_GB.json +1 -0
  19. novelwriter/assets/i18n/project_en_US.json +2 -0
  20. novelwriter/assets/i18n/project_it_IT.json +2 -0
  21. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  22. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  23. novelwriter/assets/i18n/project_nl_NL.json +2 -0
  24. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  25. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  26. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  27. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  28. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  30. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  31. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  32. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  33. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  34. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  35. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  36. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  37. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  38. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  39. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  40. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  41. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  42. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  43. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  44. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  45. novelwriter/assets/manual.pdf +0 -0
  46. novelwriter/assets/sample.zip +0 -0
  47. novelwriter/common.py +101 -3
  48. novelwriter/config.py +30 -17
  49. novelwriter/constants.py +189 -81
  50. novelwriter/core/buildsettings.py +74 -40
  51. novelwriter/core/coretools.py +146 -148
  52. novelwriter/core/docbuild.py +133 -171
  53. novelwriter/core/document.py +1 -1
  54. novelwriter/core/index.py +39 -38
  55. novelwriter/core/item.py +42 -9
  56. novelwriter/core/itemmodel.py +518 -0
  57. novelwriter/core/options.py +5 -2
  58. novelwriter/core/project.py +68 -90
  59. novelwriter/core/projectdata.py +8 -2
  60. novelwriter/core/projectxml.py +1 -1
  61. novelwriter/core/sessions.py +1 -1
  62. novelwriter/core/spellcheck.py +10 -15
  63. novelwriter/core/status.py +24 -8
  64. novelwriter/core/storage.py +1 -1
  65. novelwriter/core/tree.py +269 -288
  66. novelwriter/dialogs/about.py +1 -1
  67. novelwriter/dialogs/docmerge.py +8 -18
  68. novelwriter/dialogs/docsplit.py +1 -1
  69. novelwriter/dialogs/editlabel.py +1 -1
  70. novelwriter/dialogs/preferences.py +47 -34
  71. novelwriter/dialogs/projectsettings.py +149 -99
  72. novelwriter/dialogs/quotes.py +1 -1
  73. novelwriter/dialogs/wordlist.py +11 -10
  74. novelwriter/enum.py +37 -24
  75. novelwriter/error.py +2 -2
  76. novelwriter/extensions/configlayout.py +28 -13
  77. novelwriter/extensions/eventfilters.py +1 -1
  78. novelwriter/extensions/modified.py +30 -6
  79. novelwriter/extensions/novelselector.py +4 -3
  80. novelwriter/extensions/pagedsidebar.py +9 -9
  81. novelwriter/extensions/progressbars.py +4 -4
  82. novelwriter/extensions/statusled.py +3 -3
  83. novelwriter/extensions/switch.py +3 -3
  84. novelwriter/extensions/switchbox.py +1 -1
  85. novelwriter/extensions/versioninfo.py +1 -1
  86. novelwriter/formats/shared.py +156 -0
  87. novelwriter/formats/todocx.py +1191 -0
  88. novelwriter/formats/tohtml.py +454 -0
  89. novelwriter/{core → formats}/tokenizer.py +497 -495
  90. novelwriter/formats/tomarkdown.py +218 -0
  91. novelwriter/{core → formats}/toodt.py +312 -433
  92. novelwriter/formats/toqdoc.py +486 -0
  93. novelwriter/formats/toraw.py +91 -0
  94. novelwriter/gui/doceditor.py +347 -287
  95. novelwriter/gui/dochighlight.py +97 -85
  96. novelwriter/gui/docviewer.py +90 -33
  97. novelwriter/gui/docviewerpanel.py +18 -26
  98. novelwriter/gui/editordocument.py +18 -3
  99. novelwriter/gui/itemdetails.py +27 -29
  100. novelwriter/gui/mainmenu.py +130 -64
  101. novelwriter/gui/noveltree.py +46 -48
  102. novelwriter/gui/outline.py +202 -256
  103. novelwriter/gui/projtree.py +590 -1242
  104. novelwriter/gui/search.py +11 -19
  105. novelwriter/gui/sidebar.py +8 -7
  106. novelwriter/gui/statusbar.py +20 -3
  107. novelwriter/gui/theme.py +11 -6
  108. novelwriter/guimain.py +101 -201
  109. novelwriter/shared.py +67 -28
  110. novelwriter/text/counting.py +3 -1
  111. novelwriter/text/patterns.py +169 -61
  112. novelwriter/tools/dictionaries.py +3 -3
  113. novelwriter/tools/lipsum.py +1 -1
  114. novelwriter/tools/manusbuild.py +15 -13
  115. novelwriter/tools/manuscript.py +121 -79
  116. novelwriter/tools/manussettings.py +424 -291
  117. novelwriter/tools/noveldetails.py +1 -1
  118. novelwriter/tools/welcome.py +6 -6
  119. novelwriter/tools/writingstats.py +4 -4
  120. novelwriter/types.py +25 -9
  121. novelwriter/core/tohtml.py +0 -530
  122. novelwriter/core/tomarkdown.py +0 -252
  123. novelwriter/core/toqdoc.py +0 -419
  124. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/LICENSE.md +0 -0
  125. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/entry_points.txt +0 -0
  126. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,486 @@
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 (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
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, QFontDatabase, QPageSize, QTextBlockFormat, QTextCharFormat,
33
+ QTextCursor, QTextDocument, QTextFrameFormat
34
+ )
35
+ from PyQt5.QtPrintSupport import QPrinter
36
+
37
+ from novelwriter import __version__
38
+ from novelwriter.constants import nwStyles, nwUnicode
39
+ from novelwriter.core.project import NWProject
40
+ from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
41
+ from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
42
+ from novelwriter.types import (
43
+ QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
44
+ QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore,
45
+ QtPropLineHeight, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
46
+ )
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
51
+
52
+
53
+ def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
54
+ if cursor.position() > 0:
55
+ cursor.insertBlock(bFmt)
56
+ else:
57
+ cursor.setBlockFormat(bFmt)
58
+
59
+
60
+ class ToQTextDocument(Tokenizer):
61
+ """Core: QTextDocument Writer
62
+
63
+ Extend the Tokenizer class to generate a QTextDocument output. This
64
+ is intended for usage in the document viewer and build tool preview.
65
+ """
66
+
67
+ def __init__(self, project: NWProject) -> None:
68
+ super().__init__(project)
69
+ self._document = QTextDocument()
70
+ self._document.setUndoRedoEnabled(False)
71
+ self._document.setDocumentMargin(0.0)
72
+
73
+ self._usedNotes: dict[str, int] = {}
74
+ self._usedFields: list[tuple[int, str]] = []
75
+
76
+ self._init = False
77
+ self._newPage = False
78
+ self._anchors = True
79
+
80
+ self._hWeight = QFont.Weight.Bold
81
+ self._dWeight = QFont.Weight.Normal
82
+ self._dItalic = False
83
+ self._dStrike = False
84
+ self._dUnderline = False
85
+
86
+ self._dpi = 96
87
+ self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
88
+ self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
89
+
90
+ return
91
+
92
+ ##
93
+ # Properties
94
+ ##
95
+
96
+ @property
97
+ def document(self) -> QTextDocument:
98
+ """Return the document."""
99
+ return self._document
100
+
101
+ ##
102
+ # Setters
103
+ ##
104
+
105
+ def setPageLayout(
106
+ self, width: float, height: float, top: float, bottom: float, left: float, right: float
107
+ ) -> None:
108
+ """Set the document page size and margins in millimetres."""
109
+ self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
110
+ self._pageMargins = QMarginsF(left, top, right, bottom)
111
+ return
112
+
113
+ def setShowNewPage(self, state: bool) -> None:
114
+ """Add markers for page breaks."""
115
+ self._newPage = state
116
+ return
117
+
118
+ def disableAnchors(self) -> None:
119
+ """Disable anchors for when writing to file."""
120
+ self._anchors = False
121
+ return
122
+
123
+ ##
124
+ # Class Methods
125
+ ##
126
+
127
+ def initDocument(self, pdf: bool = False) -> None:
128
+ """Initialise all computed values of the document."""
129
+ super().initDocument()
130
+
131
+ if pdf:
132
+ fontDB = QFontDatabase()
133
+ family = self._textFont.family()
134
+ style = self._textFont.styleName()
135
+ self._dpi = 1200 if fontDB.isScalable(family, style) else 72
136
+
137
+ self._document.setUndoRedoEnabled(False)
138
+ self._document.blockSignals(True)
139
+ self._document.clear()
140
+ self._document.setDefaultFont(self._textFont)
141
+
142
+ # Default Styles
143
+ self._dWeight = self._textFont.weight()
144
+ self._dItalic = self._textFont.italic()
145
+ self._dStrike = self._textFont.strikeOut()
146
+ self._dUnderline = self._textFont.underline()
147
+
148
+ # Header Weight
149
+ self._hWeight = QFont.Weight.Bold if self._boldHeads else self._dWeight
150
+
151
+ # Scaled Sizes
152
+ # ============
153
+
154
+ fPt = self._textFont.pointSizeF()
155
+ fPx = fPt*96.0/72.0 # 1 em in pixels
156
+ mPx = fPx * self._dpi/96.0
157
+
158
+ self._mHead = {
159
+ BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
160
+ BlockTyp.PART: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
161
+ BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]),
162
+ BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]),
163
+ BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]),
164
+ BlockTyp.HEAD4: (fPx * self._marginHead4[0], fPx * self._marginHead4[1]),
165
+ }
166
+
167
+ hScale = self._scaleHeads
168
+ self._sHead = {
169
+ BlockTyp.TITLE: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
170
+ BlockTyp.PART: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
171
+ BlockTyp.HEAD1: (nwStyles.H_SIZES.get(1, 1.0) * fPt) if hScale else fPt,
172
+ BlockTyp.HEAD2: (nwStyles.H_SIZES.get(2, 1.0) * fPt) if hScale else fPt,
173
+ BlockTyp.HEAD3: (nwStyles.H_SIZES.get(3, 1.0) * fPt) if hScale else fPt,
174
+ BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt,
175
+ }
176
+
177
+ self._mText = (fPx * self._marginText[0], fPx * self._marginText[1])
178
+ self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1])
179
+ self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1])
180
+
181
+ self._mIndent = mPx * 2.0
182
+ self._tIndent = mPx * self._firstWidth
183
+
184
+ # Text Formats
185
+ # ============
186
+
187
+ self._blockFmt = QTextBlockFormat()
188
+ self._blockFmt.setTopMargin(self._mText[0])
189
+ self._blockFmt.setBottomMargin(self._mText[1])
190
+ self._blockFmt.setAlignment(QtAlignAbsolute)
191
+ self._blockFmt.setLineHeight(100.0*self._lineHeight, QtPropLineHeight)
192
+
193
+ self._charFmt = QTextCharFormat()
194
+ self._charFmt.setBackground(QtTransparent)
195
+ self._charFmt.setForeground(self._theme.text)
196
+
197
+ self._init = True
198
+
199
+ return
200
+
201
+ def doConvert(self) -> None:
202
+ """Write text tokens into the document."""
203
+ if not self._init:
204
+ return
205
+
206
+ self._document.blockSignals(True)
207
+ cursor = QTextCursor(self._document)
208
+ cursor.movePosition(QTextCursor.MoveOperation.End)
209
+
210
+ for tType, tMeta, tText, tFormat, tStyle in self._blocks:
211
+
212
+ bFmt = QTextBlockFormat(self._blockFmt)
213
+ if tType in (BlockTyp.COMMENT, BlockTyp.KEYWORD):
214
+ bFmt.setTopMargin(self._mMeta[0])
215
+ bFmt.setBottomMargin(self._mMeta[1])
216
+ elif tType == BlockTyp.SEP:
217
+ bFmt.setTopMargin(self._mSep[0])
218
+ bFmt.setBottomMargin(self._mSep[1])
219
+
220
+ if tStyle & BlockFmt.LEFT:
221
+ bFmt.setAlignment(QtAlignLeft)
222
+ elif tStyle & BlockFmt.RIGHT:
223
+ bFmt.setAlignment(QtAlignRight)
224
+ elif tStyle & BlockFmt.CENTRE:
225
+ bFmt.setAlignment(QtAlignCenter)
226
+ elif tStyle & BlockFmt.JUSTIFY:
227
+ bFmt.setAlignment(QtAlignJustify)
228
+
229
+ if tStyle & BlockFmt.PBB:
230
+ self._insertNewPageMarker(cursor)
231
+ bFmt.setPageBreakPolicy(QtPageBreakBefore)
232
+ if tStyle & BlockFmt.PBA:
233
+ bFmt.setPageBreakPolicy(QtPageBreakAfter)
234
+
235
+ if tStyle & BlockFmt.Z_BTM:
236
+ bFmt.setBottomMargin(0.0)
237
+ if tStyle & BlockFmt.Z_TOP:
238
+ bFmt.setTopMargin(0.0)
239
+
240
+ if tStyle & BlockFmt.IND_L:
241
+ bFmt.setLeftMargin(self._mIndent)
242
+ if tStyle & BlockFmt.IND_R:
243
+ bFmt.setRightMargin(self._mIndent)
244
+ if tStyle & BlockFmt.IND_T:
245
+ bFmt.setTextIndent(self._tIndent)
246
+
247
+ if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD):
248
+ newBlock(cursor, bFmt)
249
+ self._insertFragments(tText, tFormat, cursor, self._charFmt)
250
+
251
+ elif tType in HEADINGS:
252
+ bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
253
+ newBlock(cursor, bFmt)
254
+ cursor.insertText(tText, cFmt)
255
+
256
+ elif tType == BlockTyp.SEP:
257
+ newBlock(cursor, bFmt)
258
+ cursor.insertText(tText, self._charFmt)
259
+
260
+ elif tType == BlockTyp.SKIP:
261
+ newBlock(cursor, bFmt)
262
+ cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
263
+
264
+ if tStyle & BlockFmt.PBA:
265
+ self._insertNewPageMarker(cursor)
266
+
267
+ self._document.blockSignals(False)
268
+
269
+ return
270
+
271
+ def saveDocument(self, path: Path) -> None:
272
+ """Save the document as a PDF file."""
273
+ m = self._pageMargins
274
+ logger.info("Writing PDF at %d DPI", self._dpi)
275
+
276
+ printer = QPrinter(QPrinter.PrinterMode.HighResolution)
277
+ printer.setDocName(self._project.data.name)
278
+ printer.setCreator(f"novelWriter/{__version__}")
279
+ printer.setResolution(self._dpi)
280
+ printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
281
+ printer.setPageSize(self._pageSize)
282
+ printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter)
283
+ printer.setOutputFileName(str(path))
284
+
285
+ self._document.documentLayout().setPaintDevice(printer)
286
+ self._document.setPageSize(QSizeF(printer.pageRect().size()))
287
+ self._document.print(printer)
288
+
289
+ return
290
+
291
+ def closeDocument(self) -> None:
292
+ """Run close document tasks."""
293
+ self._document.blockSignals(True)
294
+
295
+ # Replace fields if there are stats available
296
+ if self._usedFields and self._counts:
297
+ cursor = QTextCursor(self._document)
298
+ for pos, field in reversed(self._usedFields):
299
+ if (value := self._counts.get(field)) is not None:
300
+ cursor.setPosition(pos, QtMoveAnchor)
301
+ cursor.setPosition(pos + 1, QtKeepAnchor)
302
+ cursor.insertText(self._formatInt(value))
303
+
304
+ # Add footnotes
305
+ if self._usedNotes:
306
+ cursor = QTextCursor(self._document)
307
+ cursor.movePosition(QTextCursor.MoveOperation.End)
308
+
309
+ bFmt, cFmt = self._genHeadStyle(BlockTyp.HEAD4, "", self._blockFmt)
310
+ newBlock(cursor, bFmt)
311
+ cursor.insertText(self._localLookup("Footnotes"), cFmt)
312
+
313
+ for key, index in self._usedNotes.items():
314
+ if content := self._footnotes.get(key):
315
+ cFmt = QTextCharFormat(self._charFmt)
316
+ cFmt.setForeground(self._theme.code)
317
+ cFmt.setAnchor(True)
318
+ cFmt.setAnchorNames([f"footnote_{index}"])
319
+ newBlock(cursor, self._blockFmt)
320
+ cursor.insertText(f"{index}. ", cFmt)
321
+ self._insertFragments(*content, cursor, self._charFmt)
322
+
323
+ self._document.blockSignals(False)
324
+
325
+ return
326
+
327
+ ##
328
+ # Internal Functions
329
+ ##
330
+
331
+ def _insertFragments(
332
+ self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
333
+ ) -> None:
334
+ """Apply formatting tags to text."""
335
+ cFmt = QTextCharFormat(dFmt)
336
+ temp = text.replace("\n", nwUnicode.U_LSEP)
337
+ start = 0
338
+ primary: QColor | None = None
339
+ for pos, fmt, data in tFmt:
340
+
341
+ # Insert buffer with previous format
342
+ cursor.insertText(temp[start:pos], cFmt)
343
+
344
+ # Construct next format
345
+ if fmt == TextFmt.B_B:
346
+ cFmt.setFontWeight(QFont.Weight.Bold)
347
+ elif fmt == TextFmt.B_E:
348
+ cFmt.setFontWeight(self._dWeight)
349
+ elif fmt == TextFmt.I_B:
350
+ cFmt.setFontItalic(True)
351
+ elif fmt == TextFmt.I_E:
352
+ cFmt.setFontItalic(self._dItalic)
353
+ elif fmt == TextFmt.D_B:
354
+ cFmt.setFontStrikeOut(True)
355
+ elif fmt == TextFmt.D_E:
356
+ cFmt.setFontStrikeOut(self._dStrike)
357
+ elif fmt == TextFmt.U_B:
358
+ cFmt.setFontUnderline(True)
359
+ elif fmt == TextFmt.U_E:
360
+ cFmt.setFontUnderline(self._dUnderline)
361
+ elif fmt == TextFmt.M_B:
362
+ cFmt.setBackground(self._theme.highlight)
363
+ elif fmt == TextFmt.M_E:
364
+ cFmt.setBackground(QtTransparent)
365
+ elif fmt == TextFmt.SUP_B:
366
+ cFmt.setVerticalAlignment(QtVAlignSuper)
367
+ elif fmt == TextFmt.SUP_E:
368
+ cFmt.setVerticalAlignment(QtVAlignNormal)
369
+ elif fmt == TextFmt.SUB_B:
370
+ cFmt.setVerticalAlignment(QtVAlignSub)
371
+ elif fmt == TextFmt.SUB_E:
372
+ cFmt.setVerticalAlignment(QtVAlignNormal)
373
+ elif fmt == TextFmt.COL_B:
374
+ if color := self._classes.get(data):
375
+ cFmt.setForeground(color)
376
+ primary = color
377
+ elif fmt == TextFmt.COL_E:
378
+ cFmt.setForeground(self._theme.text)
379
+ primary = None
380
+ elif fmt == TextFmt.ANM_B:
381
+ if self._anchors:
382
+ cFmt.setAnchor(True)
383
+ cFmt.setAnchorNames([data])
384
+ elif fmt == TextFmt.ANM_E:
385
+ if self._anchors:
386
+ cFmt.setAnchor(False)
387
+ elif fmt == TextFmt.ARF_B:
388
+ if self._anchors:
389
+ cFmt.setFontUnderline(True)
390
+ cFmt.setAnchor(True)
391
+ cFmt.setAnchorHref(data)
392
+ elif fmt == TextFmt.ARF_E:
393
+ if self._anchors:
394
+ cFmt.setFontUnderline(False)
395
+ cFmt.setAnchor(False)
396
+ cFmt.setAnchorHref("")
397
+ elif fmt == TextFmt.HRF_B:
398
+ cFmt.setForeground(self._theme.link)
399
+ cFmt.setFontUnderline(True)
400
+ cFmt.setAnchor(True)
401
+ cFmt.setAnchorHref(data)
402
+ elif fmt == TextFmt.HRF_E:
403
+ cFmt.setForeground(primary or self._theme.text)
404
+ cFmt.setFontUnderline(self._dUnderline)
405
+ cFmt.setAnchor(False)
406
+ cFmt.setAnchorHref("")
407
+ elif fmt == TextFmt.FNOTE:
408
+ xFmt = QTextCharFormat(self._charFmt)
409
+ xFmt.setForeground(self._theme.code)
410
+ xFmt.setVerticalAlignment(QtVAlignSuper)
411
+ if data in self._footnotes:
412
+ index = len(self._usedNotes) + 1
413
+ self._usedNotes[data] = index
414
+ xFmt.setAnchor(True)
415
+ xFmt.setAnchorHref(f"#footnote_{index}")
416
+ xFmt.setFontUnderline(True)
417
+ cursor.insertText(f"[{index}]", xFmt)
418
+ else:
419
+ cursor.insertText("[ERR]", cFmt)
420
+ elif fmt == TextFmt.FIELD:
421
+ if field := data.partition(":")[2]:
422
+ self._usedFields.append((cursor.position(), field))
423
+ cursor.insertText("0", cFmt)
424
+ pass
425
+
426
+ # Move pos for next pass
427
+ start = pos
428
+
429
+ # Insert whatever is left in the buffer
430
+ cursor.insertText(temp[start:], cFmt)
431
+
432
+ return
433
+
434
+ def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
435
+ """Insert a new page marker."""
436
+ if self._newPage:
437
+ bgCol = QColor(self._theme.text)
438
+ bgCol.setAlphaF(0.1)
439
+ fgCol = QColor(self._theme.text)
440
+ fgCol.setAlphaF(0.8)
441
+
442
+ fFmt = QTextFrameFormat()
443
+ fFmt.setBorderStyle(QTextFrameFormat.BorderStyle.BorderStyle_None)
444
+ fFmt.setBackground(bgCol)
445
+ fFmt.setTopMargin(self._mSep[0])
446
+ fFmt.setBottomMargin(self._mSep[1])
447
+
448
+ bFmt = QTextBlockFormat(self._blockFmt)
449
+ bFmt.setAlignment(QtAlignCenter)
450
+ bFmt.setTopMargin(0.0)
451
+ bFmt.setBottomMargin(0.0)
452
+ bFmt.setLineHeight(100.0, QtPropLineHeight)
453
+
454
+ cFmt = QTextCharFormat(self._charFmt)
455
+ cFmt.setFontItalic(False)
456
+ cFmt.setFontUnderline(False)
457
+ cFmt.setFontStrikeOut(False)
458
+ cFmt.setFontWeight(QFont.Weight.Normal)
459
+ cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
460
+ cFmt.setForeground(fgCol)
461
+
462
+ cursor.insertFrame(fFmt)
463
+ cursor.setBlockFormat(bFmt)
464
+ cursor.insertText(self._project.localLookup("New Page"), cFmt)
465
+ cursor.swap(self._document.rootFrame().lastCursorPosition())
466
+
467
+ return
468
+
469
+ def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
470
+ """Generate a heading style set."""
471
+ mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
472
+
473
+ bFmt = QTextBlockFormat(rFmt)
474
+ bFmt.setTopMargin(mTop)
475
+ bFmt.setBottomMargin(mBottom)
476
+
477
+ hCol = self._colorHeads and hType != BlockTyp.TITLE
478
+ cFmt = QTextCharFormat(self._charFmt)
479
+ cFmt.setForeground(self._theme.head if hCol else self._theme.text)
480
+ cFmt.setFontWeight(self._hWeight)
481
+ cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
482
+ if hKey and self._anchors:
483
+ cFmt.setAnchorNames([hKey])
484
+ cFmt.setAnchor(True)
485
+
486
+ 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 (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
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