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,1195 @@
1
+ """
2
+ novelWriter – DocX Text Converter
3
+ =================================
4
+
5
+ File History:
6
+ Created: 2024-10-18 [2.6b1] ToDocX
7
+ Created: 2024-10-18 [2.6b1] DocXParagraph
8
+
9
+ This file is a part of novelWriter
10
+ Copyright 2018–2024, Veronica Berglyd Olsen
11
+
12
+ This program is free software: you can redistribute it and/or modify
13
+ it under the terms of the GNU General Public License as published by
14
+ the Free Software Foundation, either version 3 of the License, or
15
+ (at your option) any later version.
16
+
17
+ This program is distributed in the hope that it will be useful, but
18
+ WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20
+ General Public License for more details.
21
+
22
+ You should have received a copy of the GNU General Public License
23
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ import re
29
+ import xml.etree.ElementTree as ET
30
+
31
+ from datetime import datetime
32
+ from pathlib import Path
33
+ from typing import NamedTuple
34
+ from zipfile import ZIP_DEFLATED, ZipFile
35
+
36
+ from PyQt5.QtCore import QMarginsF, QSizeF
37
+ from PyQt5.QtGui import QColor
38
+
39
+ from novelwriter import __version__
40
+ from novelwriter.common import firstFloat, xmlElement, xmlSubElem
41
+ from novelwriter.constants import nwHeadFmt, nwStyles
42
+ from novelwriter.core.project import NWProject
43
+ from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
44
+ from novelwriter.formats.tokenizer import Tokenizer
45
+ from novelwriter.types import QtHexRgb
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ # RegEx
50
+ RX_TEXT = re.compile(r"([\n\t])", re.UNICODE)
51
+
52
+ # Types and Relationships
53
+ OOXML_SCM = "http://schemas.openxmlformats.org"
54
+ WORD_BASE = "application/vnd.openxmlformats-officedocument.wordprocessingml"
55
+ RELS_TYPE = "application/vnd.openxmlformats-package.relationships+xml"
56
+ RELS_BASE = f"{OOXML_SCM}/officeDocument/2006/relationships"
57
+
58
+ # Main XML NameSpaces
59
+ XML_NS = {
60
+ "cp": f"{OOXML_SCM}/package/2006/metadata/core-properties",
61
+ "dc": "http://purl.org/dc/elements/1.1/",
62
+ "dcterms": "http://purl.org/dc/terms/",
63
+ "r": RELS_BASE,
64
+ "w": f"{OOXML_SCM}/wordprocessingml/2006/main",
65
+ "xml": "http://www.w3.org/XML/1998/namespace",
66
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
67
+ }
68
+ for ns, uri in XML_NS.items():
69
+ ET.register_namespace(ns, uri)
70
+
71
+
72
+ def _wTag(tag: str) -> str:
73
+ """Assemble namespace and tag name for standard w namespace."""
74
+ return f"{{{OOXML_SCM}/wordprocessingml/2006/main}}{tag}"
75
+
76
+
77
+ def _mkTag(ns: str, tag: str) -> str:
78
+ """Assemble namespace and tag name."""
79
+ if uri := XML_NS.get(ns, ""):
80
+ return f"{{{uri}}}{tag}"
81
+ logger.warning("Missing xml namespace '%s'", ns)
82
+ return tag
83
+
84
+
85
+ def _docXCol(color: QColor) -> str:
86
+ """Format a QColor as the DocX accepted value."""
87
+ return color.name(QtHexRgb).lstrip("#")
88
+
89
+
90
+ def _wText(parent: ET.Element, text: str) -> ET.Element:
91
+ """Create a text element and add the preserve flag if necessary."""
92
+ attrib = {}
93
+ if len(text) > len(text.strip()):
94
+ attrib[_mkTag("xml", "space")] = "preserve"
95
+ return xmlSubElem(parent, _wTag("t"), text, attrib=attrib)
96
+
97
+
98
+ # Cached
99
+ W_VAL = _wTag("val")
100
+
101
+ # Formatting Codes
102
+ X_BLD = 0x001 # Bold format
103
+ X_ITA = 0x002 # Italic format
104
+ X_DEL = 0x004 # Strikethrough format
105
+ X_UND = 0x008 # Underline format
106
+ X_MRK = 0x010 # Marked format
107
+ X_SUP = 0x020 # Superscript
108
+ X_SUB = 0x040 # Subscript
109
+ X_COL = 0x080 # Coloured text
110
+ X_HRF = 0x100 # Link
111
+
112
+ # Formatting Masks
113
+ M_BLD = ~X_BLD
114
+ M_ITA = ~X_ITA
115
+ M_DEL = ~X_DEL
116
+ M_UND = ~X_UND
117
+ M_MRK = ~X_MRK
118
+ M_SUP = ~X_SUP
119
+ M_SUB = ~X_SUB
120
+ M_COL = ~X_COL
121
+ M_HRF = ~X_HRF
122
+
123
+ # DocX Styles
124
+ S_NORM = "Normal"
125
+ S_TITLE = "Title"
126
+ S_HEAD1 = "Heading1"
127
+ S_HEAD2 = "Heading2"
128
+ S_HEAD3 = "Heading3"
129
+ S_HEAD4 = "Heading4"
130
+ S_SEP = "Separator"
131
+ S_META = "MetaText"
132
+ S_HEAD = "Header"
133
+ S_FNOTE = "FootnoteText"
134
+
135
+
136
+ class DocXXmlRel(NamedTuple):
137
+
138
+ rId: str
139
+ relType: str
140
+ targetMode: str | None = None
141
+
142
+
143
+ class DocXXmlFile(NamedTuple):
144
+
145
+ xml: ET.Element
146
+ path: str
147
+ contentType: str
148
+
149
+
150
+ class DocXParStyle(NamedTuple):
151
+
152
+ name: str
153
+ styleId: str
154
+ size: float
155
+ basedOn: str | None = None
156
+ nextStyle: str | None = None
157
+ before: float | None = None
158
+ after: float | None = None
159
+ left: float | None = None
160
+ line: float | None = None
161
+ indentFirst: float | None = None
162
+ align: str | None = None
163
+ default: bool = False
164
+ level: int | None = None
165
+ color: str | None = None
166
+ bold: bool = False
167
+
168
+
169
+ class ToDocX(Tokenizer):
170
+ """Core: DocX Document Writer
171
+
172
+ Extend the Tokenizer class to writer DocX Document files.
173
+ """
174
+
175
+ def __init__(self, project: NWProject) -> None:
176
+ super().__init__(project)
177
+
178
+ # Properties
179
+ self._headerFormat = ""
180
+ self._pageOffset = 0
181
+
182
+ # Internal
183
+ self._fontFamily = "Liberation Serif"
184
+ self._fontSize = 12.0
185
+ self._pageSize = QSizeF(210.0, 297.0)
186
+ self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
187
+
188
+ # Data Variables
189
+ self._pars: list[DocXParagraph] = []
190
+ self._rels: dict[str, DocXXmlRel] = {}
191
+ self._files: dict[str, DocXXmlFile] = {}
192
+ self._styles: dict[str, DocXParStyle] = {}
193
+ self._usedNotes: dict[str, int] = {}
194
+ self._usedFields: list[tuple[ET.Element, str]] = []
195
+
196
+ return
197
+
198
+ ##
199
+ # Setters
200
+ ##
201
+
202
+ def setPageLayout(
203
+ self, width: float, height: float, top: float, bottom: float, left: float, right: float
204
+ ) -> None:
205
+ """Set the document page size and margins in millimetres."""
206
+ self._pageSize = QSizeF(width, height)
207
+ self._pageMargins = QMarginsF(left, top, right, bottom)
208
+ return
209
+
210
+ def setHeaderFormat(self, format: str, offset: int) -> None:
211
+ """Set the document header format."""
212
+ self._headerFormat = format.strip()
213
+ self._pageOffset = offset
214
+ return
215
+
216
+ ##
217
+ # Class Methods
218
+ ##
219
+
220
+ def initDocument(self) -> None:
221
+ """Initialises the DocX document structure."""
222
+ super().initDocument()
223
+ self._fontFamily = self._textFont.family()
224
+ self._fontSize = self._textFont.pointSizeF()
225
+ self._generateStyles()
226
+ return
227
+
228
+ def doConvert(self) -> None:
229
+ """Convert the list of text tokens into XML elements."""
230
+ bIndent = self._fontSize * self._blockIndent
231
+
232
+ for tType, _, tText, tFormat, tStyle in self._blocks:
233
+
234
+ # Create Paragraph
235
+ par = DocXParagraph()
236
+ self._pars.append(par)
237
+
238
+ # Styles
239
+ if tStyle & BlockFmt.LEFT:
240
+ par.setAlignment("left")
241
+ elif tStyle & BlockFmt.RIGHT:
242
+ par.setAlignment("right")
243
+ elif tStyle & BlockFmt.CENTRE:
244
+ par.setAlignment("center")
245
+ elif tStyle & BlockFmt.JUSTIFY:
246
+ par.setAlignment("both")
247
+
248
+ if tStyle & BlockFmt.PBB:
249
+ par.setPageBreakBefore(True)
250
+ if tStyle & BlockFmt.PBA:
251
+ par.setPageBreakAfter(True)
252
+
253
+ if tStyle & BlockFmt.Z_BTM:
254
+ par.setMarginBottom(0.0)
255
+ if tStyle & BlockFmt.Z_TOP:
256
+ par.setMarginTop(0.0)
257
+
258
+ if tStyle & BlockFmt.IND_T:
259
+ par.setIndentFirst(True)
260
+ if tStyle & BlockFmt.IND_L:
261
+ par.setMarginLeft(bIndent)
262
+ if tStyle & BlockFmt.IND_R:
263
+ par.setMarginRight(bIndent)
264
+
265
+ # Process Text Types
266
+ if tType == BlockTyp.TEXT:
267
+ self._processFragments(par, S_NORM, tText, tFormat)
268
+
269
+ elif tType == BlockTyp.TITLE:
270
+ self._processFragments(par, S_TITLE, tText, tFormat)
271
+
272
+ elif tType == BlockTyp.HEAD1:
273
+ self._processFragments(par, S_HEAD1, tText, tFormat)
274
+
275
+ elif tType == BlockTyp.HEAD2:
276
+ self._processFragments(par, S_HEAD2, tText, tFormat)
277
+
278
+ elif tType == BlockTyp.HEAD3:
279
+ self._processFragments(par, S_HEAD3, tText, tFormat)
280
+
281
+ elif tType == BlockTyp.HEAD4:
282
+ self._processFragments(par, S_HEAD4, tText, tFormat)
283
+
284
+ elif tType == BlockTyp.SEP:
285
+ self._processFragments(par, S_SEP, tText)
286
+
287
+ elif tType == BlockTyp.SKIP:
288
+ self._processFragments(par, S_NORM, "")
289
+
290
+ elif tType == BlockTyp.COMMENT:
291
+ self._processFragments(par, S_META, tText, tFormat)
292
+
293
+ elif tType == BlockTyp.KEYWORD:
294
+ self._processFragments(par, S_META, tText, tFormat)
295
+
296
+ return
297
+
298
+ def closeDocument(self) -> None:
299
+ """Generate all the XML."""
300
+ self._coreXml()
301
+ self._appXml()
302
+ self._stylesXml()
303
+ self._fontTableXml()
304
+
305
+ fId = None
306
+ dId = None
307
+ if self._headerFormat:
308
+ dId = self._defaultHeaderXml()
309
+ fId = self._firstHeaderXml()
310
+
311
+ self._documentXml(fId, dId)
312
+ self._settingsXml()
313
+ if self._usedNotes:
314
+ self._footnotesXml()
315
+
316
+ return
317
+
318
+ def saveDocument(self, path: Path) -> None:
319
+ """Save the data to a .docx file."""
320
+ # Content Lists
321
+ cExts: list[tuple[str, str]] = []
322
+ cExts.append(("xml", "application/xml"))
323
+ cExts.append(("rels", RELS_TYPE))
324
+
325
+ cDocs: list[tuple[str, str]] = []
326
+ cDocs.append(("/_rels/.rels", RELS_TYPE))
327
+ cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE))
328
+
329
+ # Relationships XML
330
+ rRels = xmlElement("Relationships", attrib={
331
+ "xmlns": f"{OOXML_SCM}/package/2006/relationships"
332
+ })
333
+ wRels = xmlElement("Relationships", attrib={
334
+ "xmlns": f"{OOXML_SCM}/package/2006/relationships"
335
+ })
336
+ for name, rel in self._rels.items():
337
+ isRoot = name in ("core.xml", "app.xml", "document.xml")
338
+ if xml := self._files.get(name):
339
+ target = f"{xml.path}/{name}" if isRoot else name
340
+ cDocs.append((f"/{xml.path}/{name}", xml.contentType))
341
+ else:
342
+ target = name
343
+ attrib = {"Id": rel.rId, "Type": rel.relType, "Target": target}
344
+ if rel.targetMode:
345
+ attrib["TargetMode"] = rel.targetMode
346
+ xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib)
347
+
348
+ # Content Types XML
349
+ dTypes = xmlElement("Types", attrib={
350
+ "xmlns": f"{OOXML_SCM}/package/2006/content-types"
351
+ })
352
+ for name, content in cExts:
353
+ xmlSubElem(dTypes, "Default", attrib={"Extension": name, "ContentType": content})
354
+ for name, content in cDocs:
355
+ xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content})
356
+
357
+ def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
358
+ zipObj.writestr(name, ET.tostring(root, encoding="utf-8", xml_declaration=True))
359
+
360
+ with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
361
+ xmlToZip("_rels/.rels", rRels, outZip)
362
+ xmlToZip("word/_rels/document.xml.rels", wRels, outZip)
363
+ for name, rel in self._files.items():
364
+ xmlToZip(f"{rel.path}/{name}", rel.xml, outZip)
365
+ xmlToZip("[Content_Types].xml", dTypes, outZip)
366
+
367
+ return
368
+
369
+ ##
370
+ # Internal Functions
371
+ ##
372
+
373
+ def _processFragments(
374
+ self, par: DocXParagraph, pStyle: str, text: str, tFmt: T_Formats | None = None
375
+ ) -> None:
376
+ """Apply formatting tags to text."""
377
+ par.setStyle(self._styles.get(pStyle))
378
+ xFmt = 0x00
379
+ xNode = None
380
+ fStart = 0
381
+ fLink = ""
382
+ fClass = ""
383
+ for fPos, fFmt, fData in tFmt or []:
384
+
385
+ if xNode is not None:
386
+ par.addContent(xNode)
387
+ xNode = None
388
+
389
+ if temp := text[fStart:fPos]:
390
+ par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
391
+
392
+ if fFmt == TextFmt.B_B:
393
+ xFmt |= X_BLD
394
+ elif fFmt == TextFmt.B_E:
395
+ xFmt &= M_BLD
396
+ elif fFmt == TextFmt.I_B:
397
+ xFmt |= X_ITA
398
+ elif fFmt == TextFmt.I_E:
399
+ xFmt &= M_ITA
400
+ elif fFmt == TextFmt.D_B:
401
+ xFmt |= X_DEL
402
+ elif fFmt == TextFmt.D_E:
403
+ xFmt &= M_DEL
404
+ elif fFmt == TextFmt.U_B:
405
+ xFmt |= X_UND
406
+ elif fFmt == TextFmt.U_E:
407
+ xFmt &= M_UND
408
+ elif fFmt == TextFmt.M_B:
409
+ xFmt |= X_MRK
410
+ elif fFmt == TextFmt.M_E:
411
+ xFmt &= M_MRK
412
+ elif fFmt == TextFmt.SUP_B:
413
+ xFmt |= X_SUP
414
+ elif fFmt == TextFmt.SUP_E:
415
+ xFmt &= M_SUP
416
+ elif fFmt == TextFmt.SUB_B:
417
+ xFmt |= X_SUB
418
+ elif fFmt == TextFmt.SUB_E:
419
+ xFmt &= M_SUB
420
+ elif fFmt == TextFmt.COL_B:
421
+ xFmt |= X_COL
422
+ fClass = fData
423
+ elif fFmt == TextFmt.COL_E:
424
+ xFmt &= M_COL
425
+ fClass = ""
426
+ elif fFmt == TextFmt.HRF_B:
427
+ xFmt |= X_HRF
428
+ fLink = fData
429
+ elif fFmt == TextFmt.HRF_E:
430
+ xFmt &= M_HRF
431
+ fLink = ""
432
+ elif fFmt == TextFmt.FNOTE:
433
+ xNode = self._generateFootnote(fData)
434
+ elif fFmt == TextFmt.FIELD:
435
+ xNode = self._generateField(fData, xFmt)
436
+ elif fFmt == TextFmt.STRIP:
437
+ pass
438
+
439
+ # Move pos for next pass
440
+ fStart = fPos
441
+
442
+ if xNode is not None:
443
+ par.addContent(xNode)
444
+
445
+ if temp := text[fStart:]:
446
+ par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
447
+
448
+ return
449
+
450
+ def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
451
+ """Encode the text run into XML."""
452
+ xR = xmlElement(_wTag("r"))
453
+ rPr = xmlSubElem(xR, _wTag("rPr"))
454
+ if fmt & X_BLD:
455
+ xmlSubElem(rPr, _wTag("b"))
456
+ if fmt & X_ITA:
457
+ xmlSubElem(rPr, _wTag("i"))
458
+ if fmt & X_UND:
459
+ xmlSubElem(rPr, _wTag("u"), attrib={W_VAL: "single"})
460
+ if fmt & X_MRK:
461
+ xmlSubElem(rPr, _wTag("shd"), attrib={
462
+ _wTag("fill"): _docXCol(self._theme.highlight), W_VAL: "clear",
463
+ })
464
+ if fmt & X_DEL:
465
+ xmlSubElem(rPr, _wTag("strike"))
466
+ if fmt & X_SUP:
467
+ xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
468
+ if fmt & X_SUB:
469
+ xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "subscript"})
470
+ if fmt & X_COL and (color := self._classes.get(fClass)):
471
+ xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
472
+
473
+ if isinstance(text, str):
474
+ for segment in RX_TEXT.split(text):
475
+ if segment == "\n":
476
+ xmlSubElem(xR, _wTag("br"))
477
+ elif segment == "\t":
478
+ xmlSubElem(xR, _wTag("tab"))
479
+ elif segment:
480
+ _wText(xR, segment)
481
+
482
+ if fmt & X_HRF and fLink:
483
+ xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
484
+ xH = xmlElement(_wTag("hyperlink"), attrib={
485
+ _mkTag("r", "id"): self._appendExternalRel(fLink),
486
+ })
487
+ xH.append(xR)
488
+ return xH
489
+
490
+ return xR
491
+
492
+ ##
493
+ # DocX Content
494
+ ##
495
+
496
+ def _generateFootnote(self, key: str) -> ET.Element | None:
497
+ """Generate a footnote XML object."""
498
+ if key in self._footnotes:
499
+ idx = len(self._usedNotes) + 1
500
+ xR = xmlElement(_wTag("r"))
501
+ rPr = xmlSubElem(xR, _wTag("rPr"))
502
+ xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
503
+ xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
504
+ self._usedNotes[key] = idx
505
+ return xR
506
+ return None
507
+
508
+ def _generateField(self, key: str, fmt: int) -> ET.Element | None:
509
+ """Generate a data field XML object."""
510
+ if key and (field := key.partition(":")[2]):
511
+ xR = self._textRunToXml(None, fmt, "", "")
512
+ xT = _wText(xR, "0")
513
+ self._usedFields.append((xT, field))
514
+ return xR
515
+ return None
516
+
517
+ def _generateStyles(self) -> None:
518
+ """Generate usable styles."""
519
+ styles: list[DocXParStyle] = []
520
+
521
+ hScale = self._scaleHeads
522
+ hColor = _docXCol(self._theme.head) if self._colorHeads else None
523
+ fSz = self._fontSize
524
+ fnSz = 0.8 * self._fontSize
525
+ fSz0 = (nwStyles.H_SIZES[0] * fSz) if hScale else fSz
526
+ fSz1 = (nwStyles.H_SIZES[1] * fSz) if hScale else fSz
527
+ fSz2 = (nwStyles.H_SIZES[2] * fSz) if hScale else fSz
528
+ fSz3 = (nwStyles.H_SIZES[3] * fSz) if hScale else fSz
529
+ fSz4 = (nwStyles.H_SIZES[4] * fSz) if hScale else fSz
530
+
531
+ # Add Normal Style
532
+ styles.append(DocXParStyle(
533
+ name="Normal",
534
+ styleId=S_NORM,
535
+ size=fSz,
536
+ default=True,
537
+ before=fSz * self._marginText[0],
538
+ after=fSz * self._marginText[1],
539
+ line=fSz * self._lineHeight,
540
+ indentFirst=fSz * self._firstWidth,
541
+ align=self._defaultAlign,
542
+ ))
543
+
544
+ # Add Title
545
+ styles.append(DocXParStyle(
546
+ name="Title",
547
+ styleId=S_TITLE,
548
+ size=fSz0,
549
+ basedOn=S_NORM,
550
+ nextStyle=S_NORM,
551
+ before=fSz * self._marginTitle[0],
552
+ after=fSz * self._marginTitle[1],
553
+ line=fSz0 * self._lineHeight,
554
+ level=0,
555
+ bold=self._boldHeads,
556
+ ))
557
+
558
+ # Add Heading 1
559
+ styles.append(DocXParStyle(
560
+ name="Heading 1",
561
+ styleId=S_HEAD1,
562
+ size=fSz1,
563
+ basedOn=S_NORM,
564
+ nextStyle=S_NORM,
565
+ before=fSz * self._marginHead1[0],
566
+ after=fSz * self._marginHead1[1],
567
+ line=fSz1 * self._lineHeight,
568
+ level=0,
569
+ color=hColor,
570
+ bold=self._boldHeads,
571
+ ))
572
+
573
+ # Add Heading 2
574
+ styles.append(DocXParStyle(
575
+ name="Heading 2",
576
+ styleId=S_HEAD2,
577
+ size=fSz2,
578
+ basedOn=S_NORM,
579
+ nextStyle=S_NORM,
580
+ before=fSz * self._marginHead2[0],
581
+ after=fSz * self._marginHead2[1],
582
+ line=fSz2 * self._lineHeight,
583
+ level=1,
584
+ color=hColor,
585
+ bold=self._boldHeads,
586
+ ))
587
+
588
+ # Add Heading 3
589
+ styles.append(DocXParStyle(
590
+ name="Heading 3",
591
+ styleId=S_HEAD3,
592
+ size=fSz3,
593
+ basedOn=S_NORM,
594
+ nextStyle=S_NORM,
595
+ before=fSz * self._marginHead3[0],
596
+ after=fSz * self._marginHead3[1],
597
+ line=fSz3 * self._lineHeight,
598
+ level=1,
599
+ color=hColor,
600
+ bold=self._boldHeads,
601
+ ))
602
+
603
+ # Add Heading 4
604
+ styles.append(DocXParStyle(
605
+ name="Heading 4",
606
+ styleId=S_HEAD4,
607
+ size=fSz4,
608
+ basedOn=S_NORM,
609
+ nextStyle=S_NORM,
610
+ before=fSz * self._marginHead4[0],
611
+ after=fSz * self._marginHead4[1],
612
+ line=fSz4 * self._lineHeight,
613
+ level=1,
614
+ color=hColor,
615
+ bold=self._boldHeads,
616
+ ))
617
+
618
+ # Add Separator
619
+ styles.append(DocXParStyle(
620
+ name="Separator",
621
+ styleId=S_SEP,
622
+ size=fSz,
623
+ basedOn=S_NORM,
624
+ nextStyle=S_NORM,
625
+ before=fSz * self._marginSep[0],
626
+ after=fSz * self._marginSep[1],
627
+ line=fSz * self._lineHeight,
628
+ align="center",
629
+ ))
630
+
631
+ # Add Text Meta Style
632
+ styles.append(DocXParStyle(
633
+ name="Meta Text",
634
+ styleId=S_META,
635
+ size=fSz,
636
+ basedOn=S_NORM,
637
+ nextStyle=S_NORM,
638
+ before=fSz * self._marginMeta[0],
639
+ after=fSz * self._marginMeta[1],
640
+ line=fSz * self._lineHeight,
641
+ ))
642
+
643
+ # Header
644
+ styles.append(DocXParStyle(
645
+ name="Header",
646
+ styleId=S_HEAD,
647
+ size=fSz,
648
+ basedOn=S_NORM,
649
+ align="right",
650
+ ))
651
+
652
+ # Footnote
653
+ styles.append(DocXParStyle(
654
+ name="Footnote Text",
655
+ styleId=S_FNOTE,
656
+ size=fnSz,
657
+ basedOn=S_NORM,
658
+ before=0.0,
659
+ after=fnSz * self._marginFoot[1],
660
+ left=fnSz * self._marginFoot[0],
661
+ line=fnSz * self._lineHeight,
662
+ ))
663
+
664
+ # Add to Cache
665
+ for style in styles:
666
+ self._styles[style.styleId] = style
667
+
668
+ return
669
+
670
+ def _nextRelId(self) -> str:
671
+ """Generate the next unique rId."""
672
+ return f"rId{len(self._rels) + 1}"
673
+
674
+ def _appendExternalRel(self, target: str) -> str:
675
+ """Append external rel to the registry."""
676
+ if rel := self._rels.get(target):
677
+ return rel.rId
678
+ rId = self._nextRelId()
679
+ self._rels[target] = DocXXmlRel(
680
+ rId=rId,
681
+ relType=f"{RELS_BASE}/hyperlink",
682
+ targetMode="External"
683
+ )
684
+ return rId
685
+
686
+ def _appXml(self) -> str:
687
+ """Populate app.xml."""
688
+ rId = self._nextRelId()
689
+ xRoot = xmlElement("Properties", attrib={
690
+ "xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties"
691
+ })
692
+ self._rels["app.xml"] = DocXXmlRel(
693
+ rId=rId,
694
+ relType=f"{RELS_BASE}/extended-properties",
695
+ )
696
+ self._files["app.xml"] = DocXXmlFile(
697
+ xml=xRoot,
698
+ path="docProps",
699
+ contentType="application/vnd.openxmlformats-officedocument.extended-properties+xml",
700
+ )
701
+
702
+ xmlSubElem(xRoot, "TotalTime", self._project.data.editTime // 60)
703
+ xmlSubElem(xRoot, "Application", f"novelWriter/{__version__}")
704
+ if count := self._counts.get("allWords"):
705
+ xmlSubElem(xRoot, "Words", count)
706
+ if count := self._counts.get("textWordChars"):
707
+ xmlSubElem(xRoot, "Characters", count)
708
+ if count := self._counts.get("textChars"):
709
+ xmlSubElem(xRoot, "CharactersWithSpaces", count)
710
+ if count := self._counts.get("paragraphCount"):
711
+ xmlSubElem(xRoot, "Paragraphs", count)
712
+
713
+ return rId
714
+
715
+ def _coreXml(self) -> str:
716
+ """Populate app.xml."""
717
+ rId = self._nextRelId()
718
+ xRoot = xmlElement(_mkTag("cp", "coreProperties"))
719
+ self._rels["core.xml"] = DocXXmlRel(
720
+ rId=rId,
721
+ relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
722
+ )
723
+ self._files["core.xml"] = DocXXmlFile(
724
+ xml=xRoot,
725
+ path="docProps",
726
+ contentType="application/vnd.openxmlformats-package.core-properties+xml",
727
+ )
728
+
729
+ timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
730
+ tsAttr = {_mkTag("xsi", "type"): "dcterms:W3CDTF"}
731
+ xmlSubElem(xRoot, _mkTag("dcterms", "created"), timeStamp, attrib=tsAttr)
732
+ xmlSubElem(xRoot, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr)
733
+ xmlSubElem(xRoot, _mkTag("dc", "creator"), self._project.data.author)
734
+ xmlSubElem(xRoot, _mkTag("dc", "title"), self._project.data.name)
735
+ xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLocale.name())
736
+ xmlSubElem(xRoot, _mkTag("cp", "revision"), str(self._project.data.saveCount))
737
+ xmlSubElem(xRoot, _mkTag("cp", "lastModifiedBy"), self._project.data.author)
738
+
739
+ return rId
740
+
741
+ def _stylesXml(self) -> str:
742
+ """Populate styles.xml."""
743
+ rId = self._nextRelId()
744
+ xRoot = xmlElement(_wTag("styles"))
745
+ self._rels["styles.xml"] = DocXXmlRel(
746
+ rId=rId,
747
+ relType=f"{RELS_BASE}/styles",
748
+ )
749
+ self._files["styles.xml"] = DocXXmlFile(
750
+ xml=xRoot,
751
+ path="word",
752
+ contentType=f"{WORD_BASE}.styles+xml",
753
+ )
754
+
755
+ # Default Style
756
+ xStyl = xmlSubElem(xRoot, _wTag("docDefaults"))
757
+ xRDef = xmlSubElem(xStyl, _wTag("rPrDefault"))
758
+ xPDef = xmlSubElem(xStyl, _wTag("pPrDefault"))
759
+ xRPr = xmlSubElem(xRDef, _wTag("rPr"))
760
+ xPPr = xmlSubElem(xPDef, _wTag("pPr"))
761
+
762
+ size = str(int(2.0 * self._fontSize))
763
+ line = str(int(20.0 * self._lineHeight * self._fontSize))
764
+
765
+ xmlSubElem(xRPr, _wTag("rFonts"), attrib={
766
+ _wTag("ascii"): self._fontFamily,
767
+ _wTag("hAnsi"): self._fontFamily,
768
+ _wTag("cs"): self._fontFamily,
769
+ })
770
+ xmlSubElem(xRPr, _wTag("sz"), attrib={W_VAL: size})
771
+ xmlSubElem(xRPr, _wTag("szCs"), attrib={W_VAL: size})
772
+ xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLocale.name()})
773
+ xmlSubElem(xPPr, _wTag("spacing"), attrib={_wTag("line"): line})
774
+
775
+ # Paragraph Styles
776
+ for style in self._styles.values():
777
+ sAttr = {}
778
+ sAttr[_wTag("type")] = "paragraph"
779
+ sAttr[_wTag("styleId")] = style.styleId
780
+ if style.default:
781
+ sAttr[_wTag("default")] = "1"
782
+
783
+ size = firstFloat(style.size, self._fontSize)
784
+
785
+ xStyl = xmlSubElem(xRoot, _wTag("style"), attrib=sAttr)
786
+ xmlSubElem(xStyl, _wTag("name"), attrib={W_VAL: style.name})
787
+ if style.basedOn:
788
+ xmlSubElem(xStyl, _wTag("basedOn"), attrib={W_VAL: style.basedOn})
789
+ if style.nextStyle:
790
+ xmlSubElem(xStyl, _wTag("next"), attrib={W_VAL: style.nextStyle})
791
+
792
+ # pPr Node
793
+ pPr = xmlSubElem(xStyl, _wTag("pPr"))
794
+ xmlSubElem(pPr, _wTag("spacing"), attrib={
795
+ _wTag("before"): str(int(20.0 * firstFloat(style.before))),
796
+ _wTag("after"): str(int(20.0 * firstFloat(style.after))),
797
+ _wTag("line"): str(int(20.0 * firstFloat(style.line, size))),
798
+ })
799
+ if style.left is not None:
800
+ xmlSubElem(pPr, _wTag("ind"), attrib={
801
+ _wTag("left"): str(int(20.0 * style.left)),
802
+ })
803
+ if style.align:
804
+ xmlSubElem(pPr, _wTag("jc"), attrib={W_VAL: style.align})
805
+ if style.level is not None:
806
+ xmlSubElem(pPr, _wTag("outlineLvl"), attrib={W_VAL: str(style.level)})
807
+
808
+ # rPr Node
809
+ rPr = xmlSubElem(xStyl, _wTag("rPr"))
810
+ if style.bold:
811
+ xmlSubElem(rPr, _wTag("b"))
812
+ if style.color:
813
+ xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: style.color})
814
+ xmlSubElem(rPr, _wTag("sz"), attrib={W_VAL: str(int(2.0 * size))})
815
+ xmlSubElem(rPr, _wTag("szCs"), attrib={W_VAL: str(int(2.0 * size))})
816
+
817
+ # Character Style
818
+ xStyl = xmlSubElem(xRoot, _wTag("style"), attrib={
819
+ _wTag("type"): "character",
820
+ _wTag("styleId"): "InternetLink"
821
+ })
822
+ xmlSubElem(xStyl, _wTag("name"), attrib={W_VAL: "Hyperlink"})
823
+ rPr = xmlSubElem(xStyl, _wTag("rPr"))
824
+ xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(self._theme.link)})
825
+ xmlSubElem(rPr, _wTag("u"), attrib={W_VAL: "single"})
826
+
827
+ return rId
828
+
829
+ def _defaultHeaderXml(self) -> str:
830
+ """Populate header1.xml."""
831
+ rId = self._nextRelId()
832
+ xRoot = xmlElement(_wTag("hdr"))
833
+ self._rels["header1.xml"] = DocXXmlRel(
834
+ rId=rId,
835
+ relType=f"{RELS_BASE}/header",
836
+ )
837
+ self._files["header1.xml"] = DocXXmlFile(
838
+ xml=xRoot,
839
+ path="word",
840
+ contentType=f"{WORD_BASE}.header+xml",
841
+ )
842
+
843
+ xP = xmlSubElem(xRoot, _wTag("p"))
844
+ xPPr = xmlSubElem(xP, _wTag("pPr"))
845
+ xmlSubElem(xPPr, _wTag("pStyle"), attrib={W_VAL: S_HEAD})
846
+ xmlSubElem(xPPr, _wTag("jc"), attrib={W_VAL: "right"})
847
+ xmlSubElem(xPPr, _wTag("rPr"))
848
+
849
+ pre, page, post = self._headerFormat.partition(nwHeadFmt.DOC_PAGE)
850
+ pre = pre.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
851
+ pre = pre.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
852
+ post = post.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
853
+ post = post.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
854
+
855
+ wFldCT = _wTag("fldCharType")
856
+ if pre:
857
+ xR = xmlSubElem(xP, _wTag("r"))
858
+ _wText(xR, pre)
859
+ if page:
860
+ xR = xmlSubElem(xP, _wTag("r"))
861
+ xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "begin"})
862
+ xR = xmlSubElem(xP, _wTag("r"))
863
+ _wText(xR, " PAGE ")
864
+ xR = xmlSubElem(xP, _wTag("r"))
865
+ xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "separate"})
866
+ xR = xmlSubElem(xP, _wTag("r"))
867
+ _wText(xR, "0")
868
+ xR = xmlSubElem(xP, _wTag("r"))
869
+ xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "end"})
870
+ if post:
871
+ xR = xmlSubElem(xP, _wTag("r"))
872
+ _wText(xR, post)
873
+
874
+ return rId
875
+
876
+ def _firstHeaderXml(self) -> str:
877
+ """Populate header2.xml."""
878
+ rId = self._nextRelId()
879
+ xRoot = xmlElement(_wTag("hdr"))
880
+ self._rels["header2.xml"] = DocXXmlRel(
881
+ rId=rId,
882
+ relType=f"{RELS_BASE}/header",
883
+ )
884
+ self._files["header2.xml"] = DocXXmlFile(
885
+ xml=xRoot,
886
+ path="word",
887
+ contentType=f"{WORD_BASE}.header+xml",
888
+ )
889
+
890
+ xP = xmlSubElem(xRoot, _wTag("p"))
891
+ xPPr = xmlSubElem(xP, _wTag("pPr"))
892
+ xmlSubElem(xPPr, _wTag("pStyle"), attrib={W_VAL: S_HEAD})
893
+ xmlSubElem(xPPr, _wTag("jc"), attrib={W_VAL: "right"})
894
+ xmlSubElem(xPPr, _wTag("rPr"))
895
+
896
+ xR = xmlSubElem(xP, _wTag("r"))
897
+ xmlSubElem(xR, _wTag("rPr"))
898
+
899
+ return rId
900
+
901
+ def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str:
902
+ """Populate document.xml."""
903
+ rId = self._nextRelId()
904
+ xRoot = xmlElement(_wTag("document"))
905
+ xBody = xmlSubElem(xRoot, _wTag("body"))
906
+ self._rels["document.xml"] = DocXXmlRel(
907
+ rId=rId,
908
+ relType=f"{RELS_BASE}/officeDocument",
909
+ )
910
+ self._files["document.xml"] = DocXXmlFile(
911
+ xml=xRoot,
912
+ path="word",
913
+ contentType=f"{WORD_BASE}.document.main+xml",
914
+ )
915
+
916
+ # Map all Page Break Before to After where possible
917
+ pars: list[DocXParagraph] = []
918
+ for i, par in enumerate(self._pars):
919
+ if i > 0 and par.pageBreakBefore:
920
+ prev = self._pars[i-1]
921
+ par.setPageBreakBefore(False)
922
+ prev.setPageBreakAfter(True)
923
+
924
+ pars.append(par)
925
+
926
+ # Replace fields if there are stats available
927
+ if self._usedFields and self._counts:
928
+ for xField, field in self._usedFields:
929
+ if (value := self._counts.get(field)) is not None:
930
+ xField.text = self._formatInt(value)
931
+
932
+ # Write Paragraphs
933
+ for par in pars:
934
+ par.toXml(xBody)
935
+
936
+ def szScale(value: float) -> str:
937
+ return str(int(value*2.0*72.0/2.54))
938
+
939
+ # Write Settings
940
+ xSect = xmlSubElem(xBody, _wTag("sectPr"))
941
+ if hFirst and hDefault:
942
+ xmlSubElem(xSect, _wTag("headerReference"), attrib={
943
+ _wTag("type"): "first", _mkTag("r", "id"): hFirst,
944
+ })
945
+ xmlSubElem(xSect, _wTag("headerReference"), attrib={
946
+ _wTag("type"): "default", _mkTag("r", "id"): hDefault,
947
+ })
948
+
949
+ xFn = xmlSubElem(xSect, _wTag("footnotePr"))
950
+ xmlSubElem(xFn, _wTag("numFmt"), attrib={
951
+ W_VAL: "decimal",
952
+ })
953
+
954
+ xmlSubElem(xSect, _wTag("pgSz"), attrib={
955
+ _wTag("w"): szScale(self._pageSize.width()),
956
+ _wTag("h"): szScale(self._pageSize.height()),
957
+ _wTag("orient"): "portrait",
958
+ })
959
+ xmlSubElem(xSect, _wTag("pgMar"), attrib={
960
+ _wTag("top"): szScale(self._pageMargins.top()),
961
+ _wTag("right"): szScale(self._pageMargins.right()),
962
+ _wTag("bottom"): szScale(self._pageMargins.bottom()),
963
+ _wTag("left"): szScale(self._pageMargins.left()),
964
+ _wTag("header"): szScale(self._pageMargins.top()/2.0),
965
+ _wTag("footer"): "0",
966
+ _wTag("gutter"): "0",
967
+ })
968
+ xmlSubElem(xSect, _wTag("pgNumType"), attrib={
969
+ _wTag("start"): str(1 - self._pageOffset),
970
+ _wTag("fmt"): "decimal",
971
+ })
972
+ xmlSubElem(xSect, _wTag("titlePg"))
973
+
974
+ return rId
975
+
976
+ def _footnotesXml(self) -> str:
977
+ """Populate footnotes.xml."""
978
+ rId = self._nextRelId()
979
+ xRoot = xmlElement(_wTag("footnotes"))
980
+ self._rels["footnotes.xml"] = DocXXmlRel(
981
+ rId=rId,
982
+ relType=f"{RELS_BASE}/footnotes",
983
+ )
984
+ self._files["footnotes.xml"] = DocXXmlFile(
985
+ xml=xRoot,
986
+ path="word",
987
+ contentType=f"{WORD_BASE}.footnotes+xml",
988
+ )
989
+
990
+ for key, idx in self._usedNotes.items():
991
+ par = DocXParagraph()
992
+ par.setIsFootnote(True)
993
+ if content := self._footnotes.get(key):
994
+ self._processFragments(par, S_FNOTE, content[0], content[1])
995
+ par.toXml(xmlSubElem(xRoot, _wTag("footnote"), attrib={_wTag("id"): str(idx)}))
996
+
997
+ return rId
998
+
999
+ def _fontTableXml(self) -> str:
1000
+ """Populate fontTable.xml."""
1001
+ rId = self._nextRelId()
1002
+ xRoot = xmlElement(_wTag("fonts"))
1003
+ self._rels["fontTable.xml"] = DocXXmlRel(
1004
+ rId=rId,
1005
+ relType=f"{RELS_BASE}/fontTable",
1006
+ )
1007
+ self._files["fontTable.xml"] = DocXXmlFile(
1008
+ xml=xRoot,
1009
+ path="word",
1010
+ contentType=f"{WORD_BASE}.fontTable+xml",
1011
+ )
1012
+
1013
+ xFont = xmlSubElem(xRoot, _wTag("font"), attrib={
1014
+ _wTag("name"): self._textFont.family(),
1015
+ })
1016
+ xmlSubElem(xFont, _wTag("pitch"), attrib={
1017
+ W_VAL: "fixed" if self._textFont.fixedPitch() else "variable",
1018
+ })
1019
+
1020
+ return rId
1021
+
1022
+ def _settingsXml(self) -> str:
1023
+ """Populate settings.xml."""
1024
+ rId = self._nextRelId()
1025
+ xRoot = xmlElement(_wTag("settings"))
1026
+ self._rels["settings.xml"] = DocXXmlRel(
1027
+ rId=rId,
1028
+ relType=f"{RELS_BASE}/settings",
1029
+ )
1030
+ self._files["settings.xml"] = DocXXmlFile(
1031
+ xml=xRoot,
1032
+ path="word",
1033
+ contentType=f"{WORD_BASE}.settings+xml",
1034
+ )
1035
+
1036
+ xSet = xmlSubElem(xRoot, _wTag("footnotePr"))
1037
+ xmlSubElem(xSet, _wTag("numFmt"), attrib={W_VAL: "decimal"})
1038
+
1039
+ xSet = xmlSubElem(xRoot, _wTag("compat"))
1040
+ xmlSubElem(xSet, _wTag("compatSetting"), attrib={
1041
+ _wTag("name"): "compatibilityMode",
1042
+ _wTag("uri"): "http://schemas.microsoft.com/office/word",
1043
+ W_VAL: "12",
1044
+ })
1045
+
1046
+ if self._counts:
1047
+ xVars = xmlSubElem(xRoot, _wTag("docVars"))
1048
+ for key, value in self._counts.items():
1049
+ xmlSubElem(xVars, _wTag("docVar"), attrib={
1050
+ _wTag("name"): f"Manuscript{key[:1].upper()}{key[1:]}",
1051
+ W_VAL: str(value),
1052
+ })
1053
+
1054
+ return rId
1055
+
1056
+
1057
+ class DocXParagraph:
1058
+
1059
+ __slots__ = (
1060
+ "_content", "_style", "_textAlign",
1061
+ "_topMargin", "_bottomMargin", "_leftMargin", "_rightMargin",
1062
+ "_indentFirst", "_breakBefore", "_breakAfter", "_footnoteRef",
1063
+ )
1064
+
1065
+ def __init__(self) -> None:
1066
+ self._content: list[ET.Element] = []
1067
+ self._style: DocXParStyle | None = None
1068
+ self._textAlign: str | None = None
1069
+ self._topMargin: float | None = None
1070
+ self._bottomMargin: float | None = None
1071
+ self._leftMargin: float | None = None
1072
+ self._rightMargin: float | None = None
1073
+ self._indentFirst = False
1074
+ self._breakBefore = False
1075
+ self._breakAfter = False
1076
+ self._footnoteRef = False
1077
+ return
1078
+
1079
+ ##
1080
+ # Properties
1081
+ ##
1082
+
1083
+ @property
1084
+ def pageBreakBefore(self) -> bool:
1085
+ """Has page break before."""
1086
+ return self._breakBefore
1087
+
1088
+ ##
1089
+ # Setters
1090
+ ##
1091
+
1092
+ def setStyle(self, style: DocXParStyle | None) -> None:
1093
+ """Set the paragraph style."""
1094
+ self._style = style
1095
+ return
1096
+
1097
+ def setAlignment(self, value: str) -> None:
1098
+ """Set paragraph alignment."""
1099
+ if value in ("left", "center", "right", "both"):
1100
+ self._textAlign = value
1101
+ return
1102
+
1103
+ def setMarginTop(self, value: float) -> None:
1104
+ """Set margin above in pt."""
1105
+ self._topMargin = value
1106
+ return
1107
+
1108
+ def setMarginBottom(self, value: float) -> None:
1109
+ """Set margin below in pt."""
1110
+ self._bottomMargin = value
1111
+ return
1112
+
1113
+ def setMarginLeft(self, value: float) -> None:
1114
+ """Set margin left in pt."""
1115
+ self._leftMargin = value
1116
+ return
1117
+
1118
+ def setMarginRight(self, value: float) -> None:
1119
+ """Set margin right in pt."""
1120
+ self._rightMargin = value
1121
+ return
1122
+
1123
+ def setIndentFirst(self, state: bool) -> None:
1124
+ """Set first line indent."""
1125
+ self._indentFirst = state
1126
+ return
1127
+
1128
+ def setPageBreakBefore(self, state: bool) -> None:
1129
+ """Set page break before flag."""
1130
+ self._breakBefore = state
1131
+ return
1132
+
1133
+ def setPageBreakAfter(self, state: bool) -> None:
1134
+ """Set page break after flag."""
1135
+ self._breakAfter = state
1136
+ return
1137
+
1138
+ def setIsFootnote(self, state: bool) -> None:
1139
+ """Set is footnote flag."""
1140
+ self._footnoteRef = state
1141
+ return
1142
+
1143
+ ##
1144
+ # Methods
1145
+ ##
1146
+
1147
+ def addContent(self, run: ET.Element) -> None:
1148
+ """Add a run segment to the paragraph."""
1149
+ self._content.append(run)
1150
+ return
1151
+
1152
+ def toXml(self, body: ET.Element) -> None:
1153
+ """Called after all content is set."""
1154
+ if style := self._style:
1155
+ xP = xmlSubElem(body, _wTag("p"))
1156
+
1157
+ # Values
1158
+ indent = {}
1159
+ if self._indentFirst and style.indentFirst is not None:
1160
+ indent[_wTag("firstLine")] = str(int(20.0 * style.indentFirst))
1161
+ if self._leftMargin is not None:
1162
+ indent[_wTag("left")] = str(int(20.0 * self._leftMargin))
1163
+ if self._rightMargin is not None:
1164
+ indent[_wTag("right")] = str(int(20.0 * self._rightMargin))
1165
+
1166
+ # Paragraph
1167
+ pPr = xmlSubElem(xP, _wTag("pPr"))
1168
+ xmlSubElem(pPr, _wTag("pStyle"), attrib={W_VAL: style.styleId})
1169
+ if self._topMargin is not None or self._bottomMargin is not None:
1170
+ xmlSubElem(pPr, _wTag("spacing"), attrib={
1171
+ _wTag("before"): str(int(20.0 * firstFloat(self._topMargin, style.before))),
1172
+ _wTag("after"): str(int(20.0 * firstFloat(self._bottomMargin, style.after))),
1173
+ _wTag("line"): str(int(20.0 * firstFloat(style.line, style.size))),
1174
+ })
1175
+ if indent:
1176
+ xmlSubElem(pPr, _wTag("ind"), attrib=indent)
1177
+ if self._textAlign:
1178
+ xmlSubElem(pPr, _wTag("jc"), attrib={W_VAL: self._textAlign})
1179
+
1180
+ # Text
1181
+ if self._footnoteRef:
1182
+ xR = xmlSubElem(xP, _wTag("r"))
1183
+ rPr = xmlSubElem(xR, _wTag("rPr"))
1184
+ xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
1185
+ xmlSubElem(xR, _wTag("footnoteRef"))
1186
+ if self._breakBefore:
1187
+ xR = xmlSubElem(xP, _wTag("r"))
1188
+ xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
1189
+ for xR in self._content:
1190
+ xP.append(xR)
1191
+ if self._breakAfter:
1192
+ xR = xmlSubElem(xP, _wTag("r"))
1193
+ xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
1194
+
1195
+ return