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.
Files changed (64) hide show
  1. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
  2. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
  3. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  5. novelwriter/assets/i18n/project_en_GB.json +1 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +1 -0
  9. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  10. novelwriter/assets/manual.pdf +0 -0
  11. novelwriter/assets/sample.zip +0 -0
  12. novelwriter/assets/themes/default_light.conf +2 -2
  13. novelwriter/common.py +63 -0
  14. novelwriter/config.py +10 -3
  15. novelwriter/constants.py +153 -60
  16. novelwriter/core/buildsettings.py +66 -39
  17. novelwriter/core/coretools.py +34 -22
  18. novelwriter/core/docbuild.py +130 -169
  19. novelwriter/core/index.py +29 -18
  20. novelwriter/core/item.py +2 -2
  21. novelwriter/core/options.py +4 -1
  22. novelwriter/core/spellcheck.py +9 -14
  23. novelwriter/dialogs/preferences.py +45 -32
  24. novelwriter/dialogs/projectsettings.py +3 -3
  25. novelwriter/enum.py +29 -23
  26. novelwriter/extensions/configlayout.py +24 -11
  27. novelwriter/extensions/modified.py +13 -1
  28. novelwriter/extensions/pagedsidebar.py +5 -5
  29. novelwriter/formats/shared.py +155 -0
  30. novelwriter/formats/todocx.py +1195 -0
  31. novelwriter/formats/tohtml.py +452 -0
  32. novelwriter/{core → formats}/tokenizer.py +483 -485
  33. novelwriter/formats/tomarkdown.py +217 -0
  34. novelwriter/{core → formats}/toodt.py +270 -320
  35. novelwriter/formats/toqdoc.py +436 -0
  36. novelwriter/formats/toraw.py +91 -0
  37. novelwriter/gui/doceditor.py +240 -193
  38. novelwriter/gui/dochighlight.py +96 -84
  39. novelwriter/gui/docviewer.py +56 -30
  40. novelwriter/gui/docviewerpanel.py +3 -3
  41. novelwriter/gui/editordocument.py +17 -2
  42. novelwriter/gui/itemdetails.py +8 -4
  43. novelwriter/gui/mainmenu.py +121 -60
  44. novelwriter/gui/noveltree.py +35 -37
  45. novelwriter/gui/outline.py +186 -238
  46. novelwriter/gui/projtree.py +142 -131
  47. novelwriter/gui/sidebar.py +7 -6
  48. novelwriter/gui/theme.py +5 -4
  49. novelwriter/guimain.py +43 -155
  50. novelwriter/shared.py +14 -4
  51. novelwriter/text/counting.py +2 -0
  52. novelwriter/text/patterns.py +155 -59
  53. novelwriter/tools/manusbuild.py +1 -1
  54. novelwriter/tools/manuscript.py +121 -78
  55. novelwriter/tools/manussettings.py +403 -260
  56. novelwriter/tools/welcome.py +4 -4
  57. novelwriter/tools/writingstats.py +3 -3
  58. novelwriter/types.py +16 -6
  59. novelwriter/core/tohtml.py +0 -530
  60. novelwriter/core/tomarkdown.py +0 -252
  61. novelwriter/core/toqdoc.py +0 -419
  62. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
  63. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
  64. {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