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