1174 lines
39 KiB
Python
1174 lines
39 KiB
Python
"""
|
||
novelWriter – DocX Text Converter
|
||
=================================
|
||
|
||
File History:
|
||
Created: 2024-10-18 [2.6b1] ToDocX
|
||
Created: 2024-10-18 [2.6b1] DocXParagraph
|
||
|
||
This file is a part of novelWriter
|
||
Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful, but
|
||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
""" # noqa
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
import xml.etree.ElementTree as ET
|
||
|
||
from datetime import datetime
|
||
from typing import TYPE_CHECKING, NamedTuple
|
||
from zipfile import ZIP_DEFLATED, ZipFile
|
||
|
||
from PyQt6.QtCore import QMargins, QSize
|
||
|
||
from novelwriter import __version__
|
||
from novelwriter.common import firstFloat, xmlElement, xmlSubElem
|
||
from novelwriter.constants import nwHeadFmt, nwStyles
|
||
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
|
||
from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer
|
||
from novelwriter.types import QtHexRgb
|
||
|
||
if TYPE_CHECKING:
|
||
from pathlib import Path
|
||
|
||
from PyQt6.QtGui import QColor
|
||
|
||
from novelwriter.core.project import NWProject
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# RegEx
|
||
RX_TEXT = re.compile(r"([\n\t])")
|
||
|
||
# Types and Relationships
|
||
OOXML_SCM = "http://schemas.openxmlformats.org"
|
||
WORD_BASE = "application/vnd.openxmlformats-officedocument.wordprocessingml"
|
||
RELS_TYPE = "application/vnd.openxmlformats-package.relationships+xml"
|
||
RELS_BASE = f"{OOXML_SCM}/officeDocument/2006/relationships"
|
||
|
||
# Main XML NameSpaces
|
||
XML_NS = {
|
||
"cp": f"{OOXML_SCM}/package/2006/metadata/core-properties",
|
||
"dc": "http://purl.org/dc/elements/1.1/",
|
||
"dcterms": "http://purl.org/dc/terms/",
|
||
"r": RELS_BASE,
|
||
"w": f"{OOXML_SCM}/wordprocessingml/2006/main",
|
||
"xml": "http://www.w3.org/XML/1998/namespace",
|
||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||
}
|
||
for ns, uri in XML_NS.items():
|
||
ET.register_namespace(ns, uri)
|
||
|
||
|
||
def _wTag(tag: str) -> str:
|
||
"""Assemble namespace and tag name for standard w namespace."""
|
||
return f"{{{OOXML_SCM}/wordprocessingml/2006/main}}{tag}"
|
||
|
||
|
||
def _mkTag(ns: str, tag: str) -> str:
|
||
"""Assemble namespace and tag name."""
|
||
if uri := XML_NS.get(ns, ""):
|
||
return f"{{{uri}}}{tag}"
|
||
logger.warning("Missing xml namespace '%s'", ns)
|
||
return tag
|
||
|
||
|
||
def _docXCol(color: QColor) -> str:
|
||
"""Format a QColor as the DocX accepted value."""
|
||
return color.name(QtHexRgb).lstrip("#")
|
||
|
||
|
||
def _wText(parent: ET.Element, text: str) -> ET.Element:
|
||
"""Create a text element and add the preserve flag if necessary."""
|
||
attrib = {}
|
||
if len(text) > len(text.strip()):
|
||
attrib[_mkTag("xml", "space")] = "preserve"
|
||
return xmlSubElem(parent, _wTag("t"), text, attrib=attrib)
|
||
|
||
|
||
def _mmToSz(value: float) -> int:
|
||
"""Convert millimetres to internal margin size units."""
|
||
return int(value*20.0*72.0/25.4)
|
||
|
||
|
||
# Cached
|
||
W_VAL = _wTag("val")
|
||
|
||
# Formatting Codes
|
||
X_BLD = 0x001 # Bold format
|
||
X_ITA = 0x002 # Italic format
|
||
X_DEL = 0x004 # Strikethrough format
|
||
X_UND = 0x008 # Underline format
|
||
X_MRK = 0x010 # Marked format
|
||
X_SUP = 0x020 # Superscript
|
||
X_SUB = 0x040 # Subscript
|
||
X_COL = 0x080 # Coloured text
|
||
X_HRF = 0x100 # Link
|
||
|
||
# Formatting Masks
|
||
M_BLD = ~X_BLD
|
||
M_ITA = ~X_ITA
|
||
M_DEL = ~X_DEL
|
||
M_UND = ~X_UND
|
||
M_MRK = ~X_MRK
|
||
M_SUP = ~X_SUP
|
||
M_SUB = ~X_SUB
|
||
M_COL = ~X_COL
|
||
M_HRF = ~X_HRF
|
||
|
||
# DocX Styles
|
||
S_NORM = "Normal"
|
||
S_TITLE = "Title"
|
||
S_HEAD1 = "Heading1"
|
||
S_HEAD2 = "Heading2"
|
||
S_HEAD3 = "Heading3"
|
||
S_HEAD4 = "Heading4"
|
||
S_SEP = "Separator"
|
||
S_META = "MetaText"
|
||
S_HEAD = "Header"
|
||
S_FNOTE = "FootnoteText"
|
||
|
||
|
||
class DocXXmlRel(NamedTuple):
|
||
"""DocX XML Rel Data."""
|
||
|
||
rId: str
|
||
relType: str
|
||
targetMode: str | None = None
|
||
|
||
|
||
class DocXXmlFile(NamedTuple):
|
||
"""DocX XML File Data."""
|
||
|
||
xml: ET.Element
|
||
path: str
|
||
contentType: str
|
||
|
||
|
||
class DocXParStyle(NamedTuple):
|
||
"""DocX XML Paragraph Style Data."""
|
||
|
||
name: str
|
||
styleId: str
|
||
size: float
|
||
basedOn: str | None = None
|
||
nextStyle: str | None = None
|
||
before: float | None = None
|
||
after: float | None = None
|
||
left: float | None = None
|
||
line: float | None = None
|
||
indentFirst: float | None = None
|
||
align: str | None = None
|
||
default: bool = False
|
||
level: int | None = None
|
||
color: str | None = None
|
||
bold: bool = False
|
||
|
||
|
||
class ToDocX(Tokenizer):
|
||
"""Core: DocX Document Writer.
|
||
|
||
Extend the Tokenizer class to writer DocX Document files.
|
||
"""
|
||
|
||
def __init__(self, project: NWProject) -> None:
|
||
super().__init__(project)
|
||
|
||
# Properties
|
||
self._headerFormat = ""
|
||
self._pageOffset = 0
|
||
|
||
# Internal
|
||
self._fontFamily = "Liberation Serif"
|
||
self._fontSize = 12.0
|
||
self._pageSize = QSize(_mmToSz(210.0), _mmToSz(297.0))
|
||
self._pageMargins = QMargins(_mmToSz(20.0), _mmToSz(20.0), _mmToSz(20.0), _mmToSz(20.0))
|
||
|
||
# Data Variables
|
||
self._pars: list[DocXParagraph] = []
|
||
self._rels: dict[str, DocXXmlRel] = {}
|
||
self._files: dict[str, DocXXmlFile] = {}
|
||
self._styles: dict[str, DocXParStyle] = {}
|
||
self._usedNotes: dict[str, int] = {}
|
||
self._usedFields: list[tuple[ET.Element, str]] = []
|
||
|
||
##
|
||
# Setters
|
||
##
|
||
|
||
def setPageLayout(
|
||
self, width: float, height: float, top: float, bottom: float, left: float, right: float
|
||
) -> None:
|
||
"""Set the document page size and margins in millimetres."""
|
||
self._pageSize = QSize(_mmToSz(width), _mmToSz(height))
|
||
self._pageMargins = QMargins(_mmToSz(left), _mmToSz(top), _mmToSz(right), _mmToSz(bottom))
|
||
|
||
def setHeaderFormat(self, value: str, offset: int) -> None:
|
||
"""Set the document header format."""
|
||
self._headerFormat = value.strip()
|
||
self._pageOffset = offset
|
||
|
||
##
|
||
# Class Methods
|
||
##
|
||
|
||
def initDocument(self) -> None:
|
||
"""Initialise the DocX document structure."""
|
||
super().initDocument()
|
||
self._fontFamily = self._textFont.family()
|
||
self._fontSize = self._textFont.pointSizeF()
|
||
self._generateStyles()
|
||
|
||
def doConvert(self) -> None:
|
||
"""Convert the list of text tokens into XML elements."""
|
||
bIndent = self._fontSize * self._blockIndent
|
||
|
||
for tType, _, tText, tFormat, tStyle in self._blocks:
|
||
|
||
# Create Paragraph
|
||
par = DocXParagraph()
|
||
self._pars.append(par)
|
||
|
||
# Styles
|
||
if tStyle & BlockFmt.LEFT:
|
||
par.setAlignment("left")
|
||
elif tStyle & BlockFmt.RIGHT:
|
||
par.setAlignment("right")
|
||
elif tStyle & BlockFmt.CENTRE:
|
||
par.setAlignment("center")
|
||
elif tStyle & BlockFmt.JUSTIFY:
|
||
par.setAlignment("both")
|
||
|
||
if tStyle & BlockFmt.PBB:
|
||
par.setPageBreakBefore(True)
|
||
if tStyle & BlockFmt.PBA:
|
||
par.setPageBreakAfter(True)
|
||
|
||
if tStyle & BlockFmt.Z_BTM:
|
||
par.setMarginBottom(0.0)
|
||
if tStyle & BlockFmt.Z_TOP:
|
||
par.setMarginTop(0.0)
|
||
|
||
if tStyle & BlockFmt.IND_T:
|
||
par.setIndentFirst(True)
|
||
if tStyle & BlockFmt.IND_L:
|
||
par.setMarginLeft(bIndent)
|
||
if tStyle & BlockFmt.IND_R:
|
||
par.setMarginRight(bIndent)
|
||
|
||
# Process Text Types
|
||
if tType == BlockTyp.TEXT:
|
||
self._processFragments(par, S_NORM, tText, tFormat)
|
||
|
||
elif tType in (BlockTyp.TITLE, BlockTyp.PART):
|
||
self._processFragments(par, S_TITLE, tText, tFormat)
|
||
|
||
elif tType == BlockTyp.HEAD1:
|
||
self._processFragments(par, S_HEAD1, tText, tFormat)
|
||
|
||
elif tType == BlockTyp.HEAD2:
|
||
self._processFragments(par, S_HEAD2, tText, tFormat)
|
||
|
||
elif tType == BlockTyp.HEAD3:
|
||
self._processFragments(par, S_HEAD3, tText, tFormat)
|
||
|
||
elif tType == BlockTyp.HEAD4:
|
||
self._processFragments(par, S_HEAD4, tText, tFormat)
|
||
|
||
elif tType == BlockTyp.SEP:
|
||
self._processFragments(par, S_SEP, tText)
|
||
|
||
elif tType == BlockTyp.SKIP:
|
||
self._processFragments(par, S_NORM, "")
|
||
|
||
elif tType in COMMENT_BLOCKS:
|
||
self._processFragments(par, S_META, tText, tFormat)
|
||
|
||
elif tType == BlockTyp.KEYWORD:
|
||
self._processFragments(par, S_META, tText, tFormat)
|
||
|
||
def closeDocument(self) -> None:
|
||
"""Generate all the XML."""
|
||
self._coreXml()
|
||
self._appXml()
|
||
self._stylesXml()
|
||
self._fontTableXml()
|
||
|
||
fId = None
|
||
dId = None
|
||
if self._headerFormat:
|
||
dId = self._defaultHeaderXml()
|
||
fId = self._firstHeaderXml()
|
||
|
||
self._documentXml(fId, dId)
|
||
self._settingsXml()
|
||
if self._usedNotes:
|
||
self._footnotesXml()
|
||
|
||
def saveDocument(self, path: Path) -> None:
|
||
"""Save the data to a .docx file."""
|
||
# Content Lists
|
||
cExts: list[tuple[str, str]] = []
|
||
cExts.append(("xml", "application/xml"))
|
||
cExts.append(("rels", RELS_TYPE))
|
||
|
||
cDocs: list[tuple[str, str]] = []
|
||
cDocs.append(("/_rels/.rels", RELS_TYPE))
|
||
cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE))
|
||
|
||
# Relationships XML
|
||
rRels = xmlElement("Relationships", attrib={
|
||
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
|
||
})
|
||
wRels = xmlElement("Relationships", attrib={
|
||
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
|
||
})
|
||
for name, rel in self._rels.items():
|
||
isRoot = name in ("core.xml", "app.xml", "document.xml")
|
||
if xml := self._files.get(name):
|
||
target = f"{xml.path}/{name}" if isRoot else name
|
||
cDocs.append((f"/{xml.path}/{name}", xml.contentType))
|
||
else:
|
||
target = name
|
||
attrib = {"Id": rel.rId, "Type": rel.relType, "Target": target}
|
||
if rel.targetMode:
|
||
attrib["TargetMode"] = rel.targetMode
|
||
xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib)
|
||
|
||
# Content Types XML
|
||
dTypes = xmlElement("Types", attrib={
|
||
"xmlns": f"{OOXML_SCM}/package/2006/content-types"
|
||
})
|
||
for name, content in cExts:
|
||
xmlSubElem(dTypes, "Default", attrib={"Extension": name, "ContentType": content})
|
||
for name, content in cDocs:
|
||
xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content})
|
||
|
||
def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
|
||
zipObj.writestr(name, ET.tostring(root, encoding="utf-8", xml_declaration=True))
|
||
|
||
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
|
||
xmlToZip("_rels/.rels", rRels, outZip)
|
||
xmlToZip("word/_rels/document.xml.rels", wRels, outZip)
|
||
for name, rel in self._files.items():
|
||
xmlToZip(f"{rel.path}/{name}", rel.xml, outZip)
|
||
xmlToZip("[Content_Types].xml", dTypes, outZip)
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _processFragments(
|
||
self, par: DocXParagraph, pStyle: str, text: str, tFmt: T_Formats | None = None
|
||
) -> None:
|
||
"""Apply formatting tags to text."""
|
||
par.setStyle(self._styles.get(pStyle))
|
||
xFmt = 0x00
|
||
xNode = None
|
||
fStart = 0
|
||
fLink = ""
|
||
fClass = ""
|
||
for fPos, fFmt, fData in tFmt or []:
|
||
|
||
if xNode is not None:
|
||
par.addContent(xNode)
|
||
xNode = None
|
||
|
||
if temp := text[fStart:fPos]:
|
||
par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
|
||
|
||
if fFmt == TextFmt.B_B:
|
||
xFmt |= X_BLD
|
||
elif fFmt == TextFmt.B_E:
|
||
xFmt &= M_BLD
|
||
elif fFmt == TextFmt.I_B:
|
||
xFmt |= X_ITA
|
||
elif fFmt == TextFmt.I_E:
|
||
xFmt &= M_ITA
|
||
elif fFmt == TextFmt.D_B:
|
||
xFmt |= X_DEL
|
||
elif fFmt == TextFmt.D_E:
|
||
xFmt &= M_DEL
|
||
elif fFmt == TextFmt.U_B:
|
||
xFmt |= X_UND
|
||
elif fFmt == TextFmt.U_E:
|
||
xFmt &= M_UND
|
||
elif fFmt == TextFmt.M_B:
|
||
xFmt |= X_MRK
|
||
elif fFmt == TextFmt.M_E:
|
||
xFmt &= M_MRK
|
||
elif fFmt == TextFmt.SUP_B:
|
||
xFmt |= X_SUP
|
||
elif fFmt == TextFmt.SUP_E:
|
||
xFmt &= M_SUP
|
||
elif fFmt == TextFmt.SUB_B:
|
||
xFmt |= X_SUB
|
||
elif fFmt == TextFmt.SUB_E:
|
||
xFmt &= M_SUB
|
||
elif fFmt == TextFmt.COL_B:
|
||
xFmt |= X_COL
|
||
fClass = fData
|
||
elif fFmt == TextFmt.COL_E:
|
||
xFmt &= M_COL
|
||
fClass = ""
|
||
elif fFmt == TextFmt.HRF_B:
|
||
xFmt |= X_HRF
|
||
fLink = fData
|
||
elif fFmt == TextFmt.HRF_E:
|
||
xFmt &= M_HRF
|
||
fLink = ""
|
||
elif fFmt == TextFmt.FNOTE:
|
||
xNode = self._generateFootnote(fData)
|
||
elif fFmt == TextFmt.FIELD:
|
||
xNode = self._generateField(fData, xFmt)
|
||
elif fFmt == TextFmt.STRIP:
|
||
pass
|
||
|
||
# Move pos for next pass
|
||
fStart = fPos
|
||
|
||
if xNode is not None:
|
||
par.addContent(xNode)
|
||
|
||
if temp := text[fStart:]:
|
||
par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
|
||
|
||
def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
|
||
"""Encode the text run into XML."""
|
||
xR = xmlElement(_wTag("r"))
|
||
rPr = xmlSubElem(xR, _wTag("rPr"))
|
||
if fmt & X_BLD:
|
||
xmlSubElem(rPr, _wTag("b"))
|
||
if fmt & X_ITA:
|
||
xmlSubElem(rPr, _wTag("i"))
|
||
if fmt & X_UND:
|
||
xmlSubElem(rPr, _wTag("u"), attrib={W_VAL: "single"})
|
||
if fmt & X_MRK:
|
||
xmlSubElem(rPr, _wTag("shd"), attrib={
|
||
_wTag("fill"): _docXCol(self._theme.highlight), W_VAL: "clear",
|
||
})
|
||
if fmt & X_DEL:
|
||
xmlSubElem(rPr, _wTag("strike"))
|
||
if fmt & X_SUP:
|
||
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
|
||
if fmt & X_SUB:
|
||
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "subscript"})
|
||
if fmt & X_COL and (color := self._classes.get(fClass)):
|
||
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
|
||
|
||
if isinstance(text, str):
|
||
for segment in RX_TEXT.split(stripEscape(text)):
|
||
if segment == "\n":
|
||
xmlSubElem(xR, _wTag("br"))
|
||
elif segment == "\t":
|
||
xmlSubElem(xR, _wTag("tab"))
|
||
elif segment:
|
||
_wText(xR, segment)
|
||
|
||
if fmt & X_HRF and fLink:
|
||
xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
|
||
xH = xmlElement(_wTag("hyperlink"), attrib={
|
||
_mkTag("r", "id"): self._appendExternalRel(fLink),
|
||
})
|
||
xH.append(xR)
|
||
return xH
|
||
|
||
return xR
|
||
|
||
##
|
||
# DocX Content
|
||
##
|
||
|
||
def _generateFootnote(self, key: str) -> ET.Element | None:
|
||
"""Generate a footnote XML object."""
|
||
if key in self._footnotes:
|
||
idx = len(self._usedNotes) + 1
|
||
xR = xmlElement(_wTag("r"))
|
||
rPr = xmlSubElem(xR, _wTag("rPr"))
|
||
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
|
||
xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
|
||
self._usedNotes[key] = idx
|
||
return xR
|
||
return None
|
||
|
||
def _generateField(self, key: str, fmt: int) -> ET.Element | None:
|
||
"""Generate a data field XML object."""
|
||
if key and (field := key.partition(":")[2]):
|
||
xR = self._textRunToXml(None, fmt, "", "")
|
||
xT = _wText(xR, "0")
|
||
self._usedFields.append((xT, field))
|
||
return xR
|
||
return None
|
||
|
||
def _generateStyles(self) -> None:
|
||
"""Generate usable styles."""
|
||
styles: list[DocXParStyle] = []
|
||
|
||
hScale = self._scaleHeads
|
||
hColor = _docXCol(self._theme.head) if self._colorHeads else None
|
||
fSz = self._fontSize
|
||
|
||
# Add Normal Style
|
||
styles.append(DocXParStyle(
|
||
name="Normal",
|
||
styleId=S_NORM,
|
||
size=fSz,
|
||
default=True,
|
||
before=fSz * self._marginText[0],
|
||
after=fSz * self._marginText[1],
|
||
line=fSz * self._lineHeight,
|
||
indentFirst=fSz * self._firstWidth,
|
||
align=self._defaultAlign,
|
||
))
|
||
|
||
# Add Title
|
||
styles.append(DocXParStyle(
|
||
name="Title",
|
||
styleId=S_TITLE,
|
||
size=(nwStyles.H_SIZES[0] * fSz) if hScale else fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginTitle[0],
|
||
after=fSz * self._marginTitle[1],
|
||
line=fSz * self._lineHeight,
|
||
level=0,
|
||
bold=self._boldHeads,
|
||
))
|
||
|
||
# Add Heading 1
|
||
styles.append(DocXParStyle(
|
||
name="Heading 1",
|
||
styleId=S_HEAD1,
|
||
size=(nwStyles.H_SIZES[1] * fSz) if hScale else fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginHead1[0],
|
||
after=fSz * self._marginHead1[1],
|
||
line=fSz * self._lineHeight,
|
||
level=0,
|
||
color=hColor,
|
||
bold=self._boldHeads,
|
||
))
|
||
|
||
# Add Heading 2
|
||
styles.append(DocXParStyle(
|
||
name="Heading 2",
|
||
styleId=S_HEAD2,
|
||
size=(nwStyles.H_SIZES[2] * fSz) if hScale else fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginHead2[0],
|
||
after=fSz * self._marginHead2[1],
|
||
line=fSz * self._lineHeight,
|
||
level=1,
|
||
color=hColor,
|
||
bold=self._boldHeads,
|
||
))
|
||
|
||
# Add Heading 3
|
||
styles.append(DocXParStyle(
|
||
name="Heading 3",
|
||
styleId=S_HEAD3,
|
||
size=(nwStyles.H_SIZES[3] * fSz) if hScale else fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginHead3[0],
|
||
after=fSz * self._marginHead3[1],
|
||
line=fSz * self._lineHeight,
|
||
level=1,
|
||
color=hColor,
|
||
bold=self._boldHeads,
|
||
))
|
||
|
||
# Add Heading 4
|
||
styles.append(DocXParStyle(
|
||
name="Heading 4",
|
||
styleId=S_HEAD4,
|
||
size=(nwStyles.H_SIZES[4] * fSz) if hScale else fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginHead4[0],
|
||
after=fSz * self._marginHead4[1],
|
||
line=fSz * self._lineHeight,
|
||
level=1,
|
||
color=hColor,
|
||
bold=self._boldHeads,
|
||
))
|
||
|
||
# Add Separator
|
||
styles.append(DocXParStyle(
|
||
name="Separator",
|
||
styleId=S_SEP,
|
||
size=fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginSep[0],
|
||
after=fSz * self._marginSep[1],
|
||
line=fSz * self._lineHeight,
|
||
align="center",
|
||
))
|
||
|
||
# Add Text Meta Style
|
||
styles.append(DocXParStyle(
|
||
name="Meta Text",
|
||
styleId=S_META,
|
||
size=fSz,
|
||
basedOn=S_NORM,
|
||
nextStyle=S_NORM,
|
||
before=fSz * self._marginMeta[0],
|
||
after=fSz * self._marginMeta[1],
|
||
line=fSz * self._lineHeight,
|
||
))
|
||
|
||
# Header
|
||
styles.append(DocXParStyle(
|
||
name="Header",
|
||
styleId=S_HEAD,
|
||
size=fSz,
|
||
basedOn=S_NORM,
|
||
align="right",
|
||
))
|
||
|
||
# Footnote
|
||
styles.append(DocXParStyle(
|
||
name="Footnote Text",
|
||
styleId=S_FNOTE,
|
||
size=nwStyles.T_SMALL * fSz,
|
||
basedOn=S_NORM,
|
||
before=0.0,
|
||
after=fSz * self._marginFoot[1],
|
||
left=fSz * self._marginFoot[0],
|
||
line=fSz * self._lineHeight,
|
||
))
|
||
|
||
# Add to Cache
|
||
for style in styles:
|
||
self._styles[style.styleId] = style
|
||
|
||
def _nextRelId(self) -> str:
|
||
"""Generate the next unique rId."""
|
||
return f"rId{len(self._rels) + 1}"
|
||
|
||
def _appendExternalRel(self, target: str) -> str:
|
||
"""Append external rel to the registry."""
|
||
if rel := self._rels.get(target):
|
||
return rel.rId
|
||
rId = self._nextRelId()
|
||
self._rels[target] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/hyperlink",
|
||
targetMode="External"
|
||
)
|
||
return rId
|
||
|
||
def _appXml(self) -> str:
|
||
"""Populate app.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement("Properties", attrib={
|
||
"xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties"
|
||
})
|
||
self._rels["app.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/extended-properties",
|
||
)
|
||
self._files["app.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="docProps",
|
||
contentType="application/vnd.openxmlformats-officedocument.extended-properties+xml",
|
||
)
|
||
|
||
xmlSubElem(xRoot, "TotalTime", self._project.data.editTime // 60)
|
||
xmlSubElem(xRoot, "Application", f"novelWriter/{__version__}")
|
||
if count := self._counts.get("allWords"):
|
||
xmlSubElem(xRoot, "Words", count)
|
||
if count := self._counts.get("textWordChars"):
|
||
xmlSubElem(xRoot, "Characters", count)
|
||
if count := self._counts.get("textChars"):
|
||
xmlSubElem(xRoot, "CharactersWithSpaces", count)
|
||
if count := self._counts.get("paragraphCount"):
|
||
xmlSubElem(xRoot, "Paragraphs", count)
|
||
|
||
return rId
|
||
|
||
def _coreXml(self) -> str:
|
||
"""Populate app.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_mkTag("cp", "coreProperties"))
|
||
self._rels["core.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
|
||
)
|
||
self._files["core.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="docProps",
|
||
contentType="application/vnd.openxmlformats-package.core-properties+xml",
|
||
)
|
||
|
||
timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
|
||
tsAttr = {_mkTag("xsi", "type"): "dcterms:W3CDTF"}
|
||
xmlSubElem(xRoot, _mkTag("dcterms", "created"), timeStamp, attrib=tsAttr)
|
||
xmlSubElem(xRoot, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr)
|
||
xmlSubElem(xRoot, _mkTag("dc", "creator"), self._project.data.author)
|
||
xmlSubElem(xRoot, _mkTag("dc", "title"), self._project.data.name)
|
||
xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLocale.name())
|
||
xmlSubElem(xRoot, _mkTag("cp", "revision"), str(self._project.data.saveCount))
|
||
xmlSubElem(xRoot, _mkTag("cp", "lastModifiedBy"), self._project.data.author)
|
||
|
||
return rId
|
||
|
||
def _stylesXml(self) -> str:
|
||
"""Populate styles.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("styles"))
|
||
self._rels["styles.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/styles",
|
||
)
|
||
self._files["styles.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.styles+xml",
|
||
)
|
||
|
||
# Default Style
|
||
xStyl = xmlSubElem(xRoot, _wTag("docDefaults"))
|
||
xRDef = xmlSubElem(xStyl, _wTag("rPrDefault"))
|
||
xPDef = xmlSubElem(xStyl, _wTag("pPrDefault"))
|
||
xRPr = xmlSubElem(xRDef, _wTag("rPr"))
|
||
xPPr = xmlSubElem(xPDef, _wTag("pPr"))
|
||
|
||
size = str(int(2.0 * self._fontSize))
|
||
line = str(int(20.0 * self._lineHeight * self._fontSize))
|
||
|
||
xmlSubElem(xRPr, _wTag("rFonts"), attrib={
|
||
_wTag("ascii"): self._fontFamily,
|
||
_wTag("hAnsi"): self._fontFamily,
|
||
_wTag("cs"): self._fontFamily,
|
||
})
|
||
xmlSubElem(xRPr, _wTag("sz"), attrib={W_VAL: size})
|
||
xmlSubElem(xRPr, _wTag("szCs"), attrib={W_VAL: size})
|
||
xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLocale.name()})
|
||
xmlSubElem(xPPr, _wTag("spacing"), attrib={_wTag("line"): line})
|
||
|
||
# Paragraph Styles
|
||
for style in self._styles.values():
|
||
sAttr = {}
|
||
sAttr[_wTag("type")] = "paragraph"
|
||
sAttr[_wTag("styleId")] = style.styleId
|
||
if style.default:
|
||
sAttr[_wTag("default")] = "1"
|
||
|
||
size = firstFloat(style.size, self._fontSize)
|
||
|
||
xStyl = xmlSubElem(xRoot, _wTag("style"), attrib=sAttr)
|
||
xmlSubElem(xStyl, _wTag("name"), attrib={W_VAL: style.name})
|
||
if style.basedOn:
|
||
xmlSubElem(xStyl, _wTag("basedOn"), attrib={W_VAL: style.basedOn})
|
||
if style.nextStyle:
|
||
xmlSubElem(xStyl, _wTag("next"), attrib={W_VAL: style.nextStyle})
|
||
|
||
# pPr Node
|
||
pPr = xmlSubElem(xStyl, _wTag("pPr"))
|
||
xmlSubElem(pPr, _wTag("spacing"), attrib={
|
||
_wTag("before"): str(int(20.0 * firstFloat(style.before))),
|
||
_wTag("after"): str(int(20.0 * firstFloat(style.after))),
|
||
_wTag("line"): str(int(20.0 * firstFloat(style.line, size))),
|
||
_wTag("lineRule"): "auto",
|
||
})
|
||
if style.left is not None:
|
||
xmlSubElem(pPr, _wTag("ind"), attrib={
|
||
_wTag("left"): str(int(20.0 * style.left)),
|
||
})
|
||
if style.align:
|
||
xmlSubElem(pPr, _wTag("jc"), attrib={W_VAL: style.align})
|
||
if style.level is not None:
|
||
xmlSubElem(pPr, _wTag("outlineLvl"), attrib={W_VAL: str(style.level)})
|
||
|
||
# rPr Node
|
||
rPr = xmlSubElem(xStyl, _wTag("rPr"))
|
||
if style.bold:
|
||
xmlSubElem(rPr, _wTag("b"))
|
||
if style.color:
|
||
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: style.color})
|
||
xmlSubElem(rPr, _wTag("sz"), attrib={W_VAL: str(int(2.0 * size))})
|
||
xmlSubElem(rPr, _wTag("szCs"), attrib={W_VAL: str(int(2.0 * size))})
|
||
|
||
# Character Style
|
||
xStyl = xmlSubElem(xRoot, _wTag("style"), attrib={
|
||
_wTag("type"): "character",
|
||
_wTag("styleId"): "InternetLink"
|
||
})
|
||
xmlSubElem(xStyl, _wTag("name"), attrib={W_VAL: "Hyperlink"})
|
||
rPr = xmlSubElem(xStyl, _wTag("rPr"))
|
||
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(self._theme.link)})
|
||
xmlSubElem(rPr, _wTag("u"), attrib={W_VAL: "single"})
|
||
|
||
return rId
|
||
|
||
def _defaultHeaderXml(self) -> str:
|
||
"""Populate header1.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("hdr"))
|
||
self._rels["header1.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/header",
|
||
)
|
||
self._files["header1.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.header+xml",
|
||
)
|
||
|
||
xP = xmlSubElem(xRoot, _wTag("p"))
|
||
xPPr = xmlSubElem(xP, _wTag("pPr"))
|
||
xmlSubElem(xPPr, _wTag("pStyle"), attrib={W_VAL: S_HEAD})
|
||
xmlSubElem(xPPr, _wTag("jc"), attrib={W_VAL: "right"})
|
||
xmlSubElem(xPPr, _wTag("rPr"))
|
||
|
||
pre, page, post = self._headerFormat.partition(nwHeadFmt.DOC_PAGE)
|
||
pre = pre.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
|
||
pre = pre.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
|
||
post = post.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
|
||
post = post.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
|
||
|
||
wFldCT = _wTag("fldCharType")
|
||
if pre:
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
_wText(xR, pre)
|
||
if page:
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "begin"})
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("instrText"), "PAGE", attrib={_mkTag("xml", "space"): "preserve"})
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "separate"})
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "end"})
|
||
if post:
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
_wText(xR, post)
|
||
|
||
return rId
|
||
|
||
def _firstHeaderXml(self) -> str:
|
||
"""Populate header2.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("hdr"))
|
||
self._rels["header2.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/header",
|
||
)
|
||
self._files["header2.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.header+xml",
|
||
)
|
||
|
||
xP = xmlSubElem(xRoot, _wTag("p"))
|
||
xPPr = xmlSubElem(xP, _wTag("pPr"))
|
||
xmlSubElem(xPPr, _wTag("pStyle"), attrib={W_VAL: S_HEAD})
|
||
xmlSubElem(xPPr, _wTag("jc"), attrib={W_VAL: "right"})
|
||
xmlSubElem(xPPr, _wTag("rPr"))
|
||
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("rPr"))
|
||
|
||
return rId
|
||
|
||
def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str:
|
||
"""Populate document.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("document"))
|
||
xBody = xmlSubElem(xRoot, _wTag("body"))
|
||
self._rels["document.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/officeDocument",
|
||
)
|
||
self._files["document.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.document.main+xml",
|
||
)
|
||
|
||
# Map all Page Break Before to After where possible
|
||
pars: list[DocXParagraph] = []
|
||
for i, par in enumerate(self._pars):
|
||
if i > 0 and par.pageBreakBefore:
|
||
prev = self._pars[i-1]
|
||
par.setPageBreakBefore(False)
|
||
prev.setPageBreakAfter(True)
|
||
|
||
pars.append(par)
|
||
|
||
# Replace fields if there are stats available
|
||
if self._usedFields and self._counts:
|
||
for xField, field in self._usedFields:
|
||
if (value := self._counts.get(field)) is not None:
|
||
xField.text = self._formatInt(value)
|
||
|
||
# Write Paragraphs
|
||
for par in pars:
|
||
par.toXml(xBody)
|
||
|
||
# Write Settings
|
||
xSect = xmlSubElem(xBody, _wTag("sectPr"))
|
||
if hFirst and hDefault:
|
||
xmlSubElem(xSect, _wTag("headerReference"), attrib={
|
||
_wTag("type"): "first", _mkTag("r", "id"): hFirst,
|
||
})
|
||
xmlSubElem(xSect, _wTag("headerReference"), attrib={
|
||
_wTag("type"): "default", _mkTag("r", "id"): hDefault,
|
||
})
|
||
|
||
xFn = xmlSubElem(xSect, _wTag("footnotePr"))
|
||
xmlSubElem(xFn, _wTag("numFmt"), attrib={
|
||
W_VAL: "decimal",
|
||
})
|
||
|
||
xmlSubElem(xSect, _wTag("pgSz"), attrib={
|
||
_wTag("w"): str(self._pageSize.width()),
|
||
_wTag("h"): str(self._pageSize.height()),
|
||
_wTag("orient"): "portrait",
|
||
})
|
||
xmlSubElem(xSect, _wTag("pgMar"), attrib={
|
||
_wTag("top"): str(self._pageMargins.top()),
|
||
_wTag("right"): str(self._pageMargins.right()),
|
||
_wTag("bottom"): str(self._pageMargins.bottom()),
|
||
_wTag("left"): str(self._pageMargins.left()),
|
||
_wTag("header"): str(self._pageMargins.top() - int(35.0*self._fontSize)),
|
||
_wTag("footer"): "0",
|
||
_wTag("gutter"): "0",
|
||
})
|
||
xmlSubElem(xSect, _wTag("pgNumType"), attrib={
|
||
_wTag("start"): str(1 - self._pageOffset),
|
||
_wTag("fmt"): "decimal",
|
||
})
|
||
xmlSubElem(xSect, _wTag("titlePg"))
|
||
|
||
return rId
|
||
|
||
def _footnotesXml(self) -> str:
|
||
"""Populate footnotes.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("footnotes"))
|
||
self._rels["footnotes.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/footnotes",
|
||
)
|
||
self._files["footnotes.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.footnotes+xml",
|
||
)
|
||
|
||
for key, idx in self._usedNotes.items():
|
||
par = DocXParagraph()
|
||
par.setIsFootnote(True)
|
||
if content := self._footnotes.get(key):
|
||
self._processFragments(par, S_FNOTE, content[0], content[1])
|
||
par.toXml(xmlSubElem(xRoot, _wTag("footnote"), attrib={_wTag("id"): str(idx)}))
|
||
|
||
return rId
|
||
|
||
def _fontTableXml(self) -> str:
|
||
"""Populate fontTable.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("fonts"))
|
||
self._rels["fontTable.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/fontTable",
|
||
)
|
||
self._files["fontTable.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.fontTable+xml",
|
||
)
|
||
|
||
xFont = xmlSubElem(xRoot, _wTag("font"), attrib={
|
||
_wTag("name"): self._textFont.family(),
|
||
})
|
||
xmlSubElem(xFont, _wTag("pitch"), attrib={
|
||
W_VAL: "fixed" if self._textFont.fixedPitch() else "variable",
|
||
})
|
||
|
||
return rId
|
||
|
||
def _settingsXml(self) -> str:
|
||
"""Populate settings.xml."""
|
||
rId = self._nextRelId()
|
||
xRoot = xmlElement(_wTag("settings"))
|
||
self._rels["settings.xml"] = DocXXmlRel(
|
||
rId=rId,
|
||
relType=f"{RELS_BASE}/settings",
|
||
)
|
||
self._files["settings.xml"] = DocXXmlFile(
|
||
xml=xRoot,
|
||
path="word",
|
||
contentType=f"{WORD_BASE}.settings+xml",
|
||
)
|
||
|
||
xSet = xmlSubElem(xRoot, _wTag("footnotePr"))
|
||
xmlSubElem(xSet, _wTag("numFmt"), attrib={W_VAL: "decimal"})
|
||
|
||
xSet = xmlSubElem(xRoot, _wTag("compat"))
|
||
xmlSubElem(xSet, _wTag("compatSetting"), attrib={
|
||
_wTag("name"): "compatibilityMode",
|
||
_wTag("uri"): "http://schemas.microsoft.com/office/word",
|
||
W_VAL: "12",
|
||
})
|
||
|
||
if self._counts:
|
||
xVars = xmlSubElem(xRoot, _wTag("docVars"))
|
||
for key, value in self._counts.items():
|
||
xmlSubElem(xVars, _wTag("docVar"), attrib={
|
||
_wTag("name"): f"Manuscript{key[:1].upper()}{key[1:]}",
|
||
W_VAL: str(value),
|
||
})
|
||
|
||
return rId
|
||
|
||
|
||
class DocXParagraph:
|
||
"""DocX Text Paragraph.
|
||
|
||
This class holds a single paragraph of a DocX document.
|
||
"""
|
||
|
||
__slots__ = (
|
||
"_bottomMargin", "_breakAfter", "_breakBefore", "_content",
|
||
"_footnoteRef", "_indentFirst", "_leftMargin", "_rightMargin",
|
||
"_style", "_textAlign", "_topMargin",
|
||
)
|
||
|
||
def __init__(self) -> None:
|
||
self._content: list[ET.Element] = []
|
||
self._style: DocXParStyle | None = None
|
||
self._textAlign: str | None = None
|
||
self._topMargin: float | None = None
|
||
self._bottomMargin: float | None = None
|
||
self._leftMargin: float | None = None
|
||
self._rightMargin: float | None = None
|
||
self._indentFirst = False
|
||
self._breakBefore = False
|
||
self._breakAfter = False
|
||
self._footnoteRef = False
|
||
|
||
##
|
||
# Properties
|
||
##
|
||
|
||
@property
|
||
def pageBreakBefore(self) -> bool:
|
||
"""Has page break before."""
|
||
return self._breakBefore
|
||
|
||
##
|
||
# Setters
|
||
##
|
||
|
||
def setStyle(self, style: DocXParStyle | None) -> None:
|
||
"""Set the paragraph style."""
|
||
self._style = style
|
||
|
||
def setAlignment(self, value: str) -> None:
|
||
"""Set paragraph alignment."""
|
||
if value in ("left", "center", "right", "both"):
|
||
self._textAlign = value
|
||
|
||
def setMarginTop(self, value: float) -> None:
|
||
"""Set margin above in pt."""
|
||
self._topMargin = value
|
||
|
||
def setMarginBottom(self, value: float) -> None:
|
||
"""Set margin below in pt."""
|
||
self._bottomMargin = value
|
||
|
||
def setMarginLeft(self, value: float) -> None:
|
||
"""Set margin left in pt."""
|
||
self._leftMargin = value
|
||
|
||
def setMarginRight(self, value: float) -> None:
|
||
"""Set margin right in pt."""
|
||
self._rightMargin = value
|
||
|
||
def setIndentFirst(self, state: bool) -> None:
|
||
"""Set first line indent."""
|
||
self._indentFirst = state
|
||
|
||
def setPageBreakBefore(self, state: bool) -> None:
|
||
"""Set page break before flag."""
|
||
self._breakBefore = state
|
||
|
||
def setPageBreakAfter(self, state: bool) -> None:
|
||
"""Set page break after flag."""
|
||
self._breakAfter = state
|
||
|
||
def setIsFootnote(self, state: bool) -> None:
|
||
"""Set is footnote flag."""
|
||
self._footnoteRef = state
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def addContent(self, run: ET.Element) -> None:
|
||
"""Add a run segment to the paragraph."""
|
||
self._content.append(run)
|
||
|
||
def toXml(self, body: ET.Element) -> None:
|
||
"""Generate the XML. Call after all content is set."""
|
||
if style := self._style:
|
||
xP = xmlSubElem(body, _wTag("p"))
|
||
|
||
# Values
|
||
indent = {}
|
||
if self._indentFirst and style.indentFirst is not None:
|
||
indent[_wTag("firstLine")] = str(int(20.0 * style.indentFirst))
|
||
if self._leftMargin is not None:
|
||
indent[_wTag("left")] = str(int(20.0 * self._leftMargin))
|
||
if self._rightMargin is not None:
|
||
indent[_wTag("right")] = str(int(20.0 * self._rightMargin))
|
||
|
||
# Paragraph
|
||
pPr = xmlSubElem(xP, _wTag("pPr"))
|
||
xmlSubElem(pPr, _wTag("pStyle"), attrib={W_VAL: style.styleId})
|
||
if self._topMargin is not None or self._bottomMargin is not None:
|
||
xmlSubElem(pPr, _wTag("spacing"), attrib={
|
||
_wTag("before"): str(int(20.0 * firstFloat(self._topMargin, style.before))),
|
||
_wTag("after"): str(int(20.0 * firstFloat(self._bottomMargin, style.after))),
|
||
_wTag("line"): str(int(20.0 * firstFloat(style.line, style.size))),
|
||
_wTag("lineRule"): "auto",
|
||
})
|
||
if indent:
|
||
xmlSubElem(pPr, _wTag("ind"), attrib=indent)
|
||
if self._textAlign:
|
||
xmlSubElem(pPr, _wTag("jc"), attrib={W_VAL: self._textAlign})
|
||
|
||
# Text
|
||
if self._footnoteRef:
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
rPr = xmlSubElem(xR, _wTag("rPr"))
|
||
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
|
||
xmlSubElem(xR, _wTag("footnoteRef"))
|
||
if self._breakBefore:
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
|
||
for xR in self._content:
|
||
xP.append(xR)
|
||
if self._breakAfter:
|
||
xR = xmlSubElem(xP, _wTag("r"))
|
||
xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
|