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.
Files changed (83) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
  3. novelwriter/__init__.py +49 -10
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  6. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  8. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  9. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  10. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  14. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  17. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  18. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  23. novelwriter/assets/manual.pdf +0 -0
  24. novelwriter/assets/sample.zip +0 -0
  25. novelwriter/common.py +100 -2
  26. novelwriter/config.py +25 -15
  27. novelwriter/constants.py +168 -60
  28. novelwriter/core/buildsettings.py +66 -39
  29. novelwriter/core/coretools.py +145 -147
  30. novelwriter/core/docbuild.py +132 -170
  31. novelwriter/core/index.py +38 -37
  32. novelwriter/core/item.py +41 -8
  33. novelwriter/core/itemmodel.py +518 -0
  34. novelwriter/core/options.py +4 -1
  35. novelwriter/core/project.py +67 -89
  36. novelwriter/core/spellcheck.py +9 -14
  37. novelwriter/core/status.py +7 -5
  38. novelwriter/core/tree.py +268 -287
  39. novelwriter/dialogs/docmerge.py +7 -17
  40. novelwriter/dialogs/preferences.py +46 -33
  41. novelwriter/dialogs/projectsettings.py +5 -5
  42. novelwriter/enum.py +36 -23
  43. novelwriter/extensions/configlayout.py +27 -12
  44. novelwriter/extensions/modified.py +13 -1
  45. novelwriter/extensions/pagedsidebar.py +5 -5
  46. novelwriter/formats/shared.py +155 -0
  47. novelwriter/formats/todocx.py +1191 -0
  48. novelwriter/formats/tohtml.py +451 -0
  49. novelwriter/{core → formats}/tokenizer.py +487 -491
  50. novelwriter/formats/tomarkdown.py +217 -0
  51. novelwriter/{core → formats}/toodt.py +311 -432
  52. novelwriter/formats/toqdoc.py +484 -0
  53. novelwriter/formats/toraw.py +91 -0
  54. novelwriter/gui/doceditor.py +342 -284
  55. novelwriter/gui/dochighlight.py +96 -84
  56. novelwriter/gui/docviewer.py +88 -31
  57. novelwriter/gui/docviewerpanel.py +17 -25
  58. novelwriter/gui/editordocument.py +17 -2
  59. novelwriter/gui/itemdetails.py +25 -28
  60. novelwriter/gui/mainmenu.py +129 -63
  61. novelwriter/gui/noveltree.py +45 -47
  62. novelwriter/gui/outline.py +196 -249
  63. novelwriter/gui/projtree.py +594 -1241
  64. novelwriter/gui/search.py +9 -10
  65. novelwriter/gui/sidebar.py +7 -6
  66. novelwriter/gui/theme.py +10 -5
  67. novelwriter/guimain.py +100 -196
  68. novelwriter/shared.py +66 -27
  69. novelwriter/text/counting.py +2 -0
  70. novelwriter/text/patterns.py +168 -60
  71. novelwriter/tools/manusbuild.py +14 -12
  72. novelwriter/tools/manuscript.py +120 -78
  73. novelwriter/tools/manussettings.py +424 -291
  74. novelwriter/tools/welcome.py +4 -4
  75. novelwriter/tools/writingstats.py +3 -3
  76. novelwriter/types.py +23 -7
  77. novelwriter/core/tohtml.py +0 -530
  78. novelwriter/core/tomarkdown.py +0 -252
  79. novelwriter/core/toqdoc.py +0 -419
  80. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  81. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
  82. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  83. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,484 @@
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, 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.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]),
161
+ BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]),
162
+ BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]),
163
+ BlockTyp.HEAD4: (fPx * self._marginHead4[0], fPx * self._marginHead4[1]),
164
+ }
165
+
166
+ hScale = self._scaleHeads
167
+ self._sHead = {
168
+ BlockTyp.TITLE: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
169
+ BlockTyp.HEAD1: (nwStyles.H_SIZES.get(1, 1.0) * fPt) if hScale else fPt,
170
+ BlockTyp.HEAD2: (nwStyles.H_SIZES.get(2, 1.0) * fPt) if hScale else fPt,
171
+ BlockTyp.HEAD3: (nwStyles.H_SIZES.get(3, 1.0) * fPt) if hScale else fPt,
172
+ BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt,
173
+ }
174
+
175
+ self._mText = (fPx * self._marginText[0], fPx * self._marginText[1])
176
+ self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1])
177
+ self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1])
178
+
179
+ self._mIndent = mPx * 2.0
180
+ self._tIndent = mPx * self._firstWidth
181
+
182
+ # Text Formats
183
+ # ============
184
+
185
+ self._blockFmt = QTextBlockFormat()
186
+ self._blockFmt.setTopMargin(self._mText[0])
187
+ self._blockFmt.setBottomMargin(self._mText[1])
188
+ self._blockFmt.setAlignment(QtAlignAbsolute)
189
+ self._blockFmt.setLineHeight(100.0*self._lineHeight, QtPropLineHeight)
190
+
191
+ self._charFmt = QTextCharFormat()
192
+ self._charFmt.setBackground(QtTransparent)
193
+ self._charFmt.setForeground(self._theme.text)
194
+
195
+ self._init = True
196
+
197
+ return
198
+
199
+ def doConvert(self) -> None:
200
+ """Write text tokens into the document."""
201
+ if not self._init:
202
+ return
203
+
204
+ self._document.blockSignals(True)
205
+ cursor = QTextCursor(self._document)
206
+ cursor.movePosition(QTextCursor.MoveOperation.End)
207
+
208
+ for tType, tMeta, tText, tFormat, tStyle in self._blocks:
209
+
210
+ bFmt = QTextBlockFormat(self._blockFmt)
211
+ if tType in (BlockTyp.COMMENT, BlockTyp.KEYWORD):
212
+ bFmt.setTopMargin(self._mMeta[0])
213
+ bFmt.setBottomMargin(self._mMeta[1])
214
+ elif tType == BlockTyp.SEP:
215
+ bFmt.setTopMargin(self._mSep[0])
216
+ bFmt.setBottomMargin(self._mSep[1])
217
+
218
+ if tStyle & BlockFmt.LEFT:
219
+ bFmt.setAlignment(QtAlignLeft)
220
+ elif tStyle & BlockFmt.RIGHT:
221
+ bFmt.setAlignment(QtAlignRight)
222
+ elif tStyle & BlockFmt.CENTRE:
223
+ bFmt.setAlignment(QtAlignCenter)
224
+ elif tStyle & BlockFmt.JUSTIFY:
225
+ bFmt.setAlignment(QtAlignJustify)
226
+
227
+ if tStyle & BlockFmt.PBB:
228
+ self._insertNewPageMarker(cursor)
229
+ bFmt.setPageBreakPolicy(QtPageBreakBefore)
230
+ if tStyle & BlockFmt.PBA:
231
+ bFmt.setPageBreakPolicy(QtPageBreakAfter)
232
+
233
+ if tStyle & BlockFmt.Z_BTM:
234
+ bFmt.setBottomMargin(0.0)
235
+ if tStyle & BlockFmt.Z_TOP:
236
+ bFmt.setTopMargin(0.0)
237
+
238
+ if tStyle & BlockFmt.IND_L:
239
+ bFmt.setLeftMargin(self._mIndent)
240
+ if tStyle & BlockFmt.IND_R:
241
+ bFmt.setRightMargin(self._mIndent)
242
+ if tStyle & BlockFmt.IND_T:
243
+ bFmt.setTextIndent(self._tIndent)
244
+
245
+ if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD):
246
+ newBlock(cursor, bFmt)
247
+ self._insertFragments(tText, tFormat, cursor, self._charFmt)
248
+
249
+ elif tType in HEADINGS:
250
+ bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
251
+ newBlock(cursor, bFmt)
252
+ cursor.insertText(tText, cFmt)
253
+
254
+ elif tType == BlockTyp.SEP:
255
+ newBlock(cursor, bFmt)
256
+ cursor.insertText(tText, self._charFmt)
257
+
258
+ elif tType == BlockTyp.SKIP:
259
+ newBlock(cursor, bFmt)
260
+ cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
261
+
262
+ if tStyle & BlockFmt.PBA:
263
+ self._insertNewPageMarker(cursor)
264
+
265
+ self._document.blockSignals(False)
266
+
267
+ return
268
+
269
+ def saveDocument(self, path: Path) -> None:
270
+ """Save the document as a PDF file."""
271
+ m = self._pageMargins
272
+ logger.info("Writing PDF at %d DPI", self._dpi)
273
+
274
+ printer = QPrinter(QPrinter.PrinterMode.HighResolution)
275
+ printer.setDocName(self._project.data.name)
276
+ printer.setCreator(f"novelWriter/{__version__}")
277
+ printer.setResolution(self._dpi)
278
+ printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
279
+ printer.setPageSize(self._pageSize)
280
+ printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter)
281
+ printer.setOutputFileName(str(path))
282
+
283
+ self._document.documentLayout().setPaintDevice(printer)
284
+ self._document.setPageSize(QSizeF(printer.pageRect().size()))
285
+ self._document.print(printer)
286
+
287
+ return
288
+
289
+ def closeDocument(self) -> None:
290
+ """Run close document tasks."""
291
+ self._document.blockSignals(True)
292
+
293
+ # Replace fields if there are stats available
294
+ if self._usedFields and self._counts:
295
+ cursor = QTextCursor(self._document)
296
+ for pos, field in reversed(self._usedFields):
297
+ if (value := self._counts.get(field)) is not None:
298
+ cursor.setPosition(pos, QtMoveAnchor)
299
+ cursor.setPosition(pos + 1, QtKeepAnchor)
300
+ cursor.insertText(self._formatInt(value))
301
+
302
+ # Add footnotes
303
+ if self._usedNotes:
304
+ cursor = QTextCursor(self._document)
305
+ cursor.movePosition(QTextCursor.MoveOperation.End)
306
+
307
+ bFmt, cFmt = self._genHeadStyle(BlockTyp.HEAD4, "", self._blockFmt)
308
+ newBlock(cursor, bFmt)
309
+ cursor.insertText(self._localLookup("Footnotes"), cFmt)
310
+
311
+ for key, index in self._usedNotes.items():
312
+ if content := self._footnotes.get(key):
313
+ cFmt = QTextCharFormat(self._charFmt)
314
+ cFmt.setForeground(self._theme.code)
315
+ cFmt.setAnchor(True)
316
+ cFmt.setAnchorNames([f"footnote_{index}"])
317
+ newBlock(cursor, self._blockFmt)
318
+ cursor.insertText(f"{index}. ", cFmt)
319
+ self._insertFragments(*content, cursor, self._charFmt)
320
+
321
+ self._document.blockSignals(False)
322
+
323
+ return
324
+
325
+ ##
326
+ # Internal Functions
327
+ ##
328
+
329
+ def _insertFragments(
330
+ self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
331
+ ) -> None:
332
+ """Apply formatting tags to text."""
333
+ cFmt = QTextCharFormat(dFmt)
334
+ temp = text.replace("\n", nwUnicode.U_LSEP)
335
+ start = 0
336
+ primary: QColor | None = None
337
+ for pos, fmt, data in tFmt:
338
+
339
+ # Insert buffer with previous format
340
+ cursor.insertText(temp[start:pos], cFmt)
341
+
342
+ # Construct next format
343
+ if fmt == TextFmt.B_B:
344
+ cFmt.setFontWeight(QFont.Weight.Bold)
345
+ elif fmt == TextFmt.B_E:
346
+ cFmt.setFontWeight(self._dWeight)
347
+ elif fmt == TextFmt.I_B:
348
+ cFmt.setFontItalic(True)
349
+ elif fmt == TextFmt.I_E:
350
+ cFmt.setFontItalic(self._dItalic)
351
+ elif fmt == TextFmt.D_B:
352
+ cFmt.setFontStrikeOut(True)
353
+ elif fmt == TextFmt.D_E:
354
+ cFmt.setFontStrikeOut(self._dStrike)
355
+ elif fmt == TextFmt.U_B:
356
+ cFmt.setFontUnderline(True)
357
+ elif fmt == TextFmt.U_E:
358
+ cFmt.setFontUnderline(self._dUnderline)
359
+ elif fmt == TextFmt.M_B:
360
+ cFmt.setBackground(self._theme.highlight)
361
+ elif fmt == TextFmt.M_E:
362
+ cFmt.setBackground(QtTransparent)
363
+ elif fmt == TextFmt.SUP_B:
364
+ cFmt.setVerticalAlignment(QtVAlignSuper)
365
+ elif fmt == TextFmt.SUP_E:
366
+ cFmt.setVerticalAlignment(QtVAlignNormal)
367
+ elif fmt == TextFmt.SUB_B:
368
+ cFmt.setVerticalAlignment(QtVAlignSub)
369
+ elif fmt == TextFmt.SUB_E:
370
+ cFmt.setVerticalAlignment(QtVAlignNormal)
371
+ elif fmt == TextFmt.COL_B:
372
+ if color := self._classes.get(data):
373
+ cFmt.setForeground(color)
374
+ primary = color
375
+ elif fmt == TextFmt.COL_E:
376
+ cFmt.setForeground(self._theme.text)
377
+ primary = None
378
+ elif fmt == TextFmt.ANM_B:
379
+ if self._anchors:
380
+ cFmt.setAnchor(True)
381
+ cFmt.setAnchorNames([data])
382
+ elif fmt == TextFmt.ANM_E:
383
+ if self._anchors:
384
+ cFmt.setAnchor(False)
385
+ elif fmt == TextFmt.ARF_B:
386
+ if self._anchors:
387
+ cFmt.setFontUnderline(True)
388
+ cFmt.setAnchor(True)
389
+ cFmt.setAnchorHref(data)
390
+ elif fmt == TextFmt.ARF_E:
391
+ if self._anchors:
392
+ cFmt.setFontUnderline(False)
393
+ cFmt.setAnchor(False)
394
+ cFmt.setAnchorHref("")
395
+ elif fmt == TextFmt.HRF_B:
396
+ cFmt.setForeground(self._theme.link)
397
+ cFmt.setFontUnderline(True)
398
+ cFmt.setAnchor(True)
399
+ cFmt.setAnchorHref(data)
400
+ elif fmt == TextFmt.HRF_E:
401
+ cFmt.setForeground(primary or self._theme.text)
402
+ cFmt.setFontUnderline(self._dUnderline)
403
+ cFmt.setAnchor(False)
404
+ cFmt.setAnchorHref("")
405
+ elif fmt == TextFmt.FNOTE:
406
+ xFmt = QTextCharFormat(self._charFmt)
407
+ xFmt.setForeground(self._theme.code)
408
+ xFmt.setVerticalAlignment(QtVAlignSuper)
409
+ if data in self._footnotes:
410
+ index = len(self._usedNotes) + 1
411
+ self._usedNotes[data] = index
412
+ xFmt.setAnchor(True)
413
+ xFmt.setAnchorHref(f"#footnote_{index}")
414
+ xFmt.setFontUnderline(True)
415
+ cursor.insertText(f"[{index}]", xFmt)
416
+ else:
417
+ cursor.insertText("[ERR]", cFmt)
418
+ elif fmt == TextFmt.FIELD:
419
+ if field := data.partition(":")[2]:
420
+ self._usedFields.append((cursor.position(), field))
421
+ cursor.insertText("0", cFmt)
422
+ pass
423
+
424
+ # Move pos for next pass
425
+ start = pos
426
+
427
+ # Insert whatever is left in the buffer
428
+ cursor.insertText(temp[start:], cFmt)
429
+
430
+ return
431
+
432
+ def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
433
+ """Insert a new page marker."""
434
+ if self._newPage:
435
+ bgCol = QColor(self._theme.text)
436
+ bgCol.setAlphaF(0.1)
437
+ fgCol = QColor(self._theme.text)
438
+ fgCol.setAlphaF(0.8)
439
+
440
+ fFmt = QTextFrameFormat()
441
+ fFmt.setBorderStyle(QTextFrameFormat.BorderStyle.BorderStyle_None)
442
+ fFmt.setBackground(bgCol)
443
+ fFmt.setTopMargin(self._mSep[0])
444
+ fFmt.setBottomMargin(self._mSep[1])
445
+
446
+ bFmt = QTextBlockFormat(self._blockFmt)
447
+ bFmt.setAlignment(QtAlignCenter)
448
+ bFmt.setTopMargin(0.0)
449
+ bFmt.setBottomMargin(0.0)
450
+ bFmt.setLineHeight(100.0, QtPropLineHeight)
451
+
452
+ cFmt = QTextCharFormat(self._charFmt)
453
+ cFmt.setFontItalic(False)
454
+ cFmt.setFontUnderline(False)
455
+ cFmt.setFontStrikeOut(False)
456
+ cFmt.setFontWeight(QFont.Weight.Normal)
457
+ cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
458
+ cFmt.setForeground(fgCol)
459
+
460
+ cursor.insertFrame(fFmt)
461
+ cursor.setBlockFormat(bFmt)
462
+ cursor.insertText(self._project.localLookup("New Page"), cFmt)
463
+ cursor.swap(self._document.rootFrame().lastCursorPosition())
464
+
465
+ return
466
+
467
+ def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
468
+ """Generate a heading style set."""
469
+ mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
470
+
471
+ bFmt = QTextBlockFormat(rFmt)
472
+ bFmt.setTopMargin(mTop)
473
+ bFmt.setBottomMargin(mBottom)
474
+
475
+ hCol = self._colorHeads and hType != BlockTyp.TITLE
476
+ cFmt = QTextCharFormat(self._charFmt)
477
+ cFmt.setForeground(self._theme.head if hCol else self._theme.text)
478
+ cFmt.setFontWeight(self._hWeight)
479
+ cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
480
+ if hKey and self._anchors:
481
+ cFmt.setAnchorNames([hKey])
482
+ cFmt.setAnchor(True)
483
+
484
+ 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