Fix issue with line spacing in DocX

This commit is contained in:
Veronica Berglyd Olsen
2024-10-19 17:08:24 +02:00
parent 34127d78bf
commit 3c76d0e7e3
+187 -160
View File
@@ -29,10 +29,11 @@ import xml.etree.ElementTree as ET
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import NamedTuple
from zipfile import ZipFile from zipfile import ZipFile
from novelwriter import __version__ from novelwriter import __version__
from novelwriter.common import xmlIndent from novelwriter.common import firstFloat, xmlIndent, xmlSubElem
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwStyles from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwStyles
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.formats.tokenizer import T_Formats, Tokenizer from novelwriter.formats.tokenizer import T_Formats, Tokenizer
@@ -75,19 +76,6 @@ def _mkTag(ns: str, tag: str) -> str:
return tag return tag
def _addSingle(
parent: ET.Element,
tag: str,
text: str | int | None = None,
attrib: dict | None = None
) -> None:
"""Add a single value to a parent element."""
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
if text is not None:
xSub.text = str(text)
return
# Formatting Codes # Formatting Codes
X_BLD = 0x001 # Bold format X_BLD = 0x001 # Bold format
X_ITA = 0x002 # Italic format X_ITA = 0x002 # Italic format
@@ -130,6 +118,22 @@ COL_META_TXT = "813709"
COL_MARK_TXT = "ffffa6" COL_MARK_TXT = "ffffa6"
class DocXParStyle(NamedTuple):
name: str
styleId: str
size: float
basedOn: str | None = None
nextStyle: str | None = None
before: float | None = None
after: float | None = None
line: float | None = None
align: str | None = None
default: bool = False
level: int | None = None
color: str | None = None
class ToDocX(Tokenizer): class ToDocX(Tokenizer):
"""Core: DocX Document Writer """Core: DocX Document Writer
@@ -154,6 +158,9 @@ class ToDocX(Tokenizer):
self._fontSize = 12.0 self._fontSize = 12.0
self._dLanguage = "en-GB" self._dLanguage = "en-GB"
# Maps
self._styles: dict[str, DocXParStyle] = {}
return return
## ##
@@ -194,7 +201,7 @@ class ToDocX(Tokenizer):
self._dDoc = ET.Element(_wTag("document")) self._dDoc = ET.Element(_wTag("document"))
self._dStyl = ET.Element(_wTag("styles")) self._dStyl = ET.Element(_wTag("styles"))
self._xBody = ET.SubElement(self._dDoc, _wTag("body")) self._xBody = xmlSubElem(self._dDoc, _wTag("body"))
self._defaultStyles() self._defaultStyles()
self._useableStyles() self._useableStyles()
@@ -295,76 +302,76 @@ class ToDocX(Tokenizer):
# .rels # .rels
dRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS}) dRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS})
_addSingle(dRels, "Relationship", attrib={ xmlSubElem(dRels, "Relationship", attrib={
"Id": "rId1", "Type": REL_CORE, "Target": "docProps/core.xml", "Id": "rId1", "Type": REL_CORE, "Target": "docProps/core.xml",
}) })
_addSingle(dRels, "Relationship", attrib={ xmlSubElem(dRels, "Relationship", attrib={
"Id": "rId2", "Type": f"{REL_BASE}/extended-properties", "Target": "docProps/app.xml", "Id": "rId2", "Type": f"{REL_BASE}/extended-properties", "Target": "docProps/app.xml",
}) })
_addSingle(dRels, "Relationship", attrib={ xmlSubElem(dRels, "Relationship", attrib={
"Id": "rId3", "Type": f"{REL_BASE}/officeDocument", "Target": "word/document.xml", "Id": "rId3", "Type": f"{REL_BASE}/officeDocument", "Target": "word/document.xml",
}) })
# core.xml # core.xml
dCore = ET.Element("coreProperties") dCore = ET.Element("coreProperties")
tsAttr = {_mkTag("xsi", "type"): "dcterms:W3CDTF"} tsAttr = {_mkTag("xsi", "type"): "dcterms:W3CDTF"}
_addSingle(dCore, _mkTag("dcterms", "created"), timeStamp, attrib=tsAttr) xmlSubElem(dCore, _mkTag("dcterms", "created"), timeStamp, attrib=tsAttr)
_addSingle(dCore, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr) xmlSubElem(dCore, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr)
_addSingle(dCore, _mkTag("dc", "creator"), self._project.data.author) xmlSubElem(dCore, _mkTag("dc", "creator"), self._project.data.author)
_addSingle(dCore, _mkTag("dc", "title"), self._project.data.name) xmlSubElem(dCore, _mkTag("dc", "title"), self._project.data.name)
_addSingle(dCore, _mkTag("dc", "creator"), self._project.data.author) xmlSubElem(dCore, _mkTag("dc", "creator"), self._project.data.author)
_addSingle(dCore, _mkTag("dc", "language"), self._dLanguage) xmlSubElem(dCore, _mkTag("dc", "language"), self._dLanguage)
_addSingle(dCore, _mkTag("cp", "revision"), str(self._project.data.saveCount)) xmlSubElem(dCore, _mkTag("cp", "revision"), str(self._project.data.saveCount))
_addSingle(dCore, _mkTag("cp", "lastModifiedBy"), self._project.data.author) xmlSubElem(dCore, _mkTag("cp", "lastModifiedBy"), self._project.data.author)
# app.xml # app.xml
dApp = ET.Element("Properties", attrib={"xmlns": PROPS_NS}) dApp = ET.Element("Properties", attrib={"xmlns": PROPS_NS})
_addSingle(dApp, "TotalTime", self._project.data.editTime // 60) xmlSubElem(dApp, "TotalTime", self._project.data.editTime // 60)
_addSingle(dApp, "Application", f"novelWriter/{__version__}") xmlSubElem(dApp, "Application", f"novelWriter/{__version__}")
if count := self._counts.get("allWords"): if count := self._counts.get("allWords"):
_addSingle(dApp, "Words", count) xmlSubElem(dApp, "Words", count)
if count := self._counts.get("textWordChars"): if count := self._counts.get("textWordChars"):
_addSingle(dApp, "Characters", count) xmlSubElem(dApp, "Characters", count)
if count := self._counts.get("textChars"): if count := self._counts.get("textChars"):
_addSingle(dApp, "CharactersWithSpaces", count) xmlSubElem(dApp, "CharactersWithSpaces", count)
if count := self._counts.get("paragraphCount"): if count := self._counts.get("paragraphCount"):
_addSingle(dApp, "Paragraphs", count) xmlSubElem(dApp, "Paragraphs", count)
# document.xml.rels # document.xml.rels
dDRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS}) dDRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS})
_addSingle(dDRels, "Relationship", attrib={ xmlSubElem(dDRels, "Relationship", attrib={
"Id": "rId1", "Type": f"{REL_BASE}/styles", "Target": "styles.xml", "Id": "rId1", "Type": f"{REL_BASE}/styles", "Target": "styles.xml",
}) })
# [Content_Types].xml # [Content_Types].xml
dCont = ET.Element("Types", attrib={"xmlns": TYPES_NS}) dCont = ET.Element("Types", attrib={"xmlns": TYPES_NS})
_addSingle(dCont, "Default", attrib={ xmlSubElem(dCont, "Default", attrib={
"Extension": "xml", "ContentType": "application/xml", "Extension": "xml", "ContentType": "application/xml",
}) })
_addSingle(dCont, "Default", attrib={ xmlSubElem(dCont, "Default", attrib={
"Extension": "rels", "ContentType": RELS_TYPE, "Extension": "rels", "ContentType": RELS_TYPE,
}) })
_addSingle(dCont, "Override", attrib={ xmlSubElem(dCont, "Override", attrib={
"PartName": "/_rels/.rels", "PartName": "/_rels/.rels",
"ContentType": RELS_TYPE, "ContentType": RELS_TYPE,
}) })
_addSingle(dCont, "Override", attrib={ xmlSubElem(dCont, "Override", attrib={
"PartName": "/docProps/core.xml", "PartName": "/docProps/core.xml",
"ContentType": f"{WORD_BASE}.extended-properties+xml", "ContentType": f"{WORD_BASE}.extended-properties+xml",
}) })
_addSingle(dCont, "Override", attrib={ xmlSubElem(dCont, "Override", attrib={
"PartName": "/docProps/app.xml", "PartName": "/docProps/app.xml",
"ContentType": "application/vnd.openxmlformats-package.core-properties+xml", "ContentType": "application/vnd.openxmlformats-package.core-properties+xml",
}) })
_addSingle(dCont, "Override", attrib={ xmlSubElem(dCont, "Override", attrib={
"PartName": "/word/_rels/document.xml.rels", "PartName": "/word/_rels/document.xml.rels",
"ContentType": RELS_TYPE, "ContentType": RELS_TYPE,
}) })
_addSingle(dCont, "Override", attrib={ xmlSubElem(dCont, "Override", attrib={
"PartName": "/word/document.xml", "PartName": "/word/document.xml",
"ContentType": f"{WORD_BASE}.wordprocessingml.document.main+xml", "ContentType": f"{WORD_BASE}.wordprocessingml.document.main+xml",
}) })
_addSingle(dCont, "Override", attrib={ xmlSubElem(dCont, "Override", attrib={
"PartName": "/word/styles.xml", "PartName": "/word/styles.xml",
"ContentType": f"{WORD_BASE}.wordprocessingml.styles+xml", "ContentType": f"{WORD_BASE}.wordprocessingml.styles+xml",
}) })
@@ -428,7 +435,7 @@ class ToDocX(Tokenizer):
self, par: DocXParagraph, pStyle: str, text: str, tFmt: T_Formats | None = None self, par: DocXParagraph, pStyle: str, text: str, tFmt: T_Formats | None = None
) -> None: ) -> None:
"""Apply formatting tags to text.""" """Apply formatting tags to text."""
par.setStyle(pStyle) par.setStyle(self._styles.get(pStyle))
xFmt = 0x00 xFmt = 0x00
fStart = 0 fStart = 0
for fPos, fFmt, fData in tFmt or []: for fPos, fFmt, fData in tFmt or []:
@@ -487,27 +494,27 @@ class ToDocX(Tokenizer):
def _textRunToXml(self, text: str, fmt: int) -> ET.Element: def _textRunToXml(self, text: str, fmt: int) -> ET.Element:
"""Encode the text run into XML.""" """Encode the text run into XML."""
run = ET.Element(_wTag("r")) run = ET.Element(_wTag("r"))
rPr = ET.SubElement(run, _wTag("rPr")) rPr = xmlSubElem(run, _wTag("rPr"))
if fmt & X_BLD == X_BLD: if fmt & X_BLD == X_BLD:
ET.SubElement(rPr, _wTag("b")) xmlSubElem(rPr, _wTag("b"))
if fmt & X_ITA == X_ITA: if fmt & X_ITA == X_ITA:
ET.SubElement(rPr, _wTag("i")) xmlSubElem(rPr, _wTag("i"))
if fmt & X_UND == X_UND: if fmt & X_UND == X_UND:
ET.SubElement(rPr, _wTag("u"), attrib={_wTag("val"): "single"}) xmlSubElem(rPr, _wTag("u"), attrib={_wTag("val"): "single"})
if fmt & X_MRK == X_MRK: if fmt & X_MRK == X_MRK:
ET.SubElement(rPr, _wTag("shd"), attrib={ xmlSubElem(rPr, _wTag("shd"), attrib={
_wTag("fill"): COL_MARK_TXT, _wTag("val"): "clear", _wTag("fill"): COL_MARK_TXT, _wTag("val"): "clear",
}) })
if fmt & X_DEL == X_DEL: if fmt & X_DEL == X_DEL:
ET.SubElement(rPr, _wTag("strike")) xmlSubElem(rPr, _wTag("strike"))
if fmt & X_SUP == X_SUP: if fmt & X_SUP == X_SUP:
ET.SubElement(rPr, _wTag("vertAlign"), attrib={_wTag("val"): "superscript"}) xmlSubElem(rPr, _wTag("vertAlign"), attrib={_wTag("val"): "superscript"})
if fmt & X_SUB == X_SUB: if fmt & X_SUB == X_SUB:
ET.SubElement(rPr, _wTag("vertAlign"), attrib={_wTag("val"): "subscript"}) xmlSubElem(rPr, _wTag("vertAlign"), attrib={_wTag("val"): "subscript"})
if fmt & X_DLG == X_DLG: if fmt & X_DLG == X_DLG:
ET.SubElement(rPr, _wTag("color"), attrib={_wTag("val"): COL_DIALOG_M}) xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): COL_DIALOG_M})
if fmt & X_DLA == X_DLA: if fmt & X_DLA == X_DLA:
ET.SubElement(rPr, _wTag("color"), attrib={_wTag("val"): COL_DIALOG_A}) xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): COL_DIALOG_A})
remaining = text remaining = text
while (parts := remaining.partition("\n"))[0]: while (parts := remaining.partition("\n"))[0]:
@@ -515,9 +522,9 @@ class ToDocX(Tokenizer):
attr = {} attr = {}
if len(segment) != len(segment.strip()): if len(segment) != len(segment.strip()):
attr[_mkTag("xml", "space")] = "preserve" attr[_mkTag("xml", "space")] = "preserve"
_addSingle(run, _wTag("t"), segment, attrib=attr) xmlSubElem(run, _wTag("t"), segment, attrib=attr)
if parts[1]: if parts[1]:
_addSingle(run, _wTag("br")) xmlSubElem(run, _wTag("br"))
remaining = parts[2] remaining = parts[2]
return run return run
@@ -528,24 +535,24 @@ class ToDocX(Tokenizer):
def _defaultStyles(self) -> None: def _defaultStyles(self) -> None:
"""Set the default styles.""" """Set the default styles."""
xStyl = ET.SubElement(self._dStyl, _wTag("docDefaults")) xStyl = xmlSubElem(self._dStyl, _wTag("docDefaults"))
xRDef = ET.SubElement(xStyl, _wTag("rPrDefault")) xRDef = xmlSubElem(xStyl, _wTag("rPrDefault"))
xPDef = ET.SubElement(xStyl, _wTag("pPrDefault")) xPDef = xmlSubElem(xStyl, _wTag("pPrDefault"))
xRPr = ET.SubElement(xRDef, _wTag("rPr")) xRPr = xmlSubElem(xRDef, _wTag("rPr"))
xPPr = ET.SubElement(xPDef, _wTag("pPr")) xPPr = xmlSubElem(xPDef, _wTag("pPr"))
size = str(int(2.0 * self._fontSize)) size = str(int(2.0 * self._fontSize))
line = str(int(20.0 * self._lineHeight * self._fontSize)) line = str(int(20.0 * self._lineHeight * self._fontSize))
ET.SubElement(xRPr, _wTag("rFonts"), attrib={ xmlSubElem(xRPr, _wTag("rFonts"), attrib={
_wTag("ascii"): self._fontFamily, _wTag("ascii"): self._fontFamily,
_wTag("hAnsi"): self._fontFamily, _wTag("hAnsi"): self._fontFamily,
_wTag("cs"): self._fontFamily, _wTag("cs"): self._fontFamily,
}) })
ET.SubElement(xRPr, _wTag("sz"), attrib={_wTag("val"): size}) xmlSubElem(xRPr, _wTag("sz"), attrib={_wTag("val"): size})
ET.SubElement(xRPr, _wTag("szCs"), attrib={_wTag("val"): size}) xmlSubElem(xRPr, _wTag("szCs"), attrib={_wTag("val"): size})
ET.SubElement(xRPr, _wTag("lang"), attrib={_wTag("val"): self._dLanguage}) xmlSubElem(xRPr, _wTag("lang"), attrib={_wTag("val"): self._dLanguage})
ET.SubElement(xPPr, _wTag("spacing"), attrib={_wTag("line"): line}) xmlSubElem(xPPr, _wTag("spacing"), attrib={_wTag("line"): line})
return return
@@ -553,143 +560,156 @@ class ToDocX(Tokenizer):
"""Set the usable styles.""" """Set the usable styles."""
hScale = self._scaleHeads hScale = self._scaleHeads
hColor = self._colorHeads hColor = self._colorHeads
fSz = self._fontSize
fSz0 = (nwStyles.H_SIZES[0] * fSz) if hScale else fSz
fSz1 = (nwStyles.H_SIZES[1] * fSz) if hScale else fSz
fSz2 = (nwStyles.H_SIZES[2] * fSz) if hScale else fSz
fSz3 = (nwStyles.H_SIZES[3] * fSz) if hScale else fSz
fSz4 = (nwStyles.H_SIZES[4] * fSz) if hScale else fSz
# Add Normal Style # Add Normal Style
self._addParStyle( self._addParStyle(DocXParStyle(
name="Normal", name="Normal",
styleId=S_NORM, styleId=S_NORM,
size=fSz,
default=True, default=True,
margins=self._marginText, before=fSz * self._marginText[0],
) after=fSz * self._marginText[1],
line=fSz * self._lineHeight,
))
# Add Title # Add Title
self._addParStyle( self._addParStyle(DocXParStyle(
name="Title", name="Title",
styleId=S_TITLE, styleId=S_TITLE,
size=nwStyles.H_SIZES[0] if hScale else 1.0, size=fSz0,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginTitle, before=fSz * self._marginTitle[0],
after=fSz * self._marginTitle[1],
line=fSz0 * self._lineHeight,
level=0, level=0,
) ))
# Add Heading 1 # Add Heading 1
self._addParStyle( self._addParStyle(DocXParStyle(
name="Heading 1", name="Heading 1",
styleId=S_HEAD1, styleId=S_HEAD1,
size=nwStyles.H_SIZES[1] if hScale else 1.0, size=fSz1,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginHead1, before=fSz * self._marginHead1[0],
after=fSz * self._marginHead1[1],
line=fSz1 * self._lineHeight,
level=0, level=0,
color=COL_HEAD_L12 if hColor else None, color=COL_HEAD_L12 if hColor else None,
) ))
# Add Heading 2 # Add Heading 2
self._addParStyle( self._addParStyle(DocXParStyle(
name="Heading 2", name="Heading 2",
styleId=S_HEAD2, styleId=S_HEAD2,
size=nwStyles.H_SIZES[2] if hScale else 1.0, size=fSz2,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginHead2, before=fSz * self._marginHead2[0],
after=fSz * self._marginHead2[1],
line=fSz2 * self._lineHeight,
level=1, level=1,
color=COL_HEAD_L12 if hColor else None, color=COL_HEAD_L12 if hColor else None,
) ))
# Add Heading 3 # Add Heading 3
self._addParStyle( self._addParStyle(DocXParStyle(
name="Heading 3", name="Heading 3",
styleId=S_HEAD3, styleId=S_HEAD3,
size=nwStyles.H_SIZES[3] if hScale else 1.0, size=fSz3,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginHead3, before=fSz * self._marginHead3[0],
after=fSz * self._marginHead3[1],
line=fSz3 * self._lineHeight,
level=1, level=1,
color=COL_HEAD_L34 if hColor else None, color=COL_HEAD_L34 if hColor else None,
) ))
# Add Heading 4 # Add Heading 4
self._addParStyle( self._addParStyle(DocXParStyle(
name="Heading 4", name="Heading 4",
styleId=S_HEAD4, styleId=S_HEAD4,
size=nwStyles.H_SIZES[4] if hScale else 1.0, size=fSz4,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginHead4, before=fSz * self._marginHead4[0],
after=fSz * self._marginHead4[1],
line=fSz4 * self._lineHeight,
level=1, level=1,
color=COL_HEAD_L34 if hColor else None, color=COL_HEAD_L34 if hColor else None,
) ))
# Add Separator # Add Separator
self._addParStyle( self._addParStyle(DocXParStyle(
name="Separator", name="Separator",
styleId=S_SEP, styleId=S_SEP,
size=fSz,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginSep, before=fSz * self._marginSep[0],
after=fSz * self._marginSep[1],
line=fSz * self._lineHeight,
align="center", align="center",
) ))
# Add Text Meta Style # Add Text Meta Style
self._addParStyle( self._addParStyle(DocXParStyle(
name="Text Meta", name="Text Meta",
styleId=S_META, styleId=S_META,
size=fSz,
basedOn=S_NORM, basedOn=S_NORM,
nextStyle=S_NORM, nextStyle=S_NORM,
margins=self._marginMeta, before=fSz * self._marginMeta[0],
after=fSz * self._marginMeta[1],
line=fSz * self._lineHeight,
color=COL_META_TXT, color=COL_META_TXT,
) ))
return return
def _addParStyle( def _addParStyle(self, style: DocXParStyle) -> None:
self, *,
name: str,
styleId: str,
size: float = 1.0,
basedOn: str | None = None,
nextStyle: str | None = None,
margins: tuple[float, float] | None = None,
align: str | None = None,
default: bool = False,
level: int | None = None,
color: str | None = None,
) -> None:
"""Add a paragraph style.""" """Add a paragraph style."""
sAttr = {} sAttr = {}
sAttr[_wTag("type")] = "paragraph" sAttr[_wTag("type")] = "paragraph"
sAttr[_wTag("styleId")] = styleId sAttr[_wTag("styleId")] = style.styleId
if default: if style.default:
sAttr[_wTag("default")] = "1" sAttr[_wTag("default")] = "1"
sz = str(int(2.0 * size * self._fontSize)) size = firstFloat(style.size, self._fontSize)
ln = str(int(20.0 * size * self._lineHeight * self._fontSize))
xStyl = ET.SubElement(self._dStyl, _wTag("style"), attrib=sAttr) xStyl = xmlSubElem(self._dStyl, _wTag("style"), attrib=sAttr)
ET.SubElement(xStyl, _wTag("name"), attrib={_wTag("val"): name}) xmlSubElem(xStyl, _wTag("name"), attrib={_wTag("val"): style.name})
if basedOn: if style.basedOn:
ET.SubElement(xStyl, _wTag("basedOn"), attrib={_wTag("val"): basedOn}) xmlSubElem(xStyl, _wTag("basedOn"), attrib={_wTag("val"): style.basedOn})
if nextStyle: if style.nextStyle:
ET.SubElement(xStyl, _wTag("next"), attrib={_wTag("val"): nextStyle}) xmlSubElem(xStyl, _wTag("next"), attrib={_wTag("val"): style.nextStyle})
if level is not None: if style.level is not None:
ET.SubElement(xStyl, _wTag("outlineLvl"), attrib={_wTag("val"): str(level)}) xmlSubElem(xStyl, _wTag("outlineLvl"), attrib={_wTag("val"): str(style.level)})
pPr = ET.SubElement(xStyl, _wTag("pPr")) pPr = xmlSubElem(xStyl, _wTag("pPr"))
if margins: xmlSubElem(pPr, _wTag("spacing"), attrib={
ET.SubElement(pPr, _wTag("spacing"), attrib={ _wTag("before"): str(int(20.0 * firstFloat(style.before))),
_wTag("before"): str(int(20.0 * margins[0] * self._fontSize)), _wTag("after"): str(int(20.0 * firstFloat(style.after))),
_wTag("after"): str(int(20.0 * margins[1] * self._fontSize)), _wTag("line"): str(int(20.0 * firstFloat(style.line, size))),
_wTag("line"): ln, })
}) if style.align:
if align: xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): style.align})
ET.SubElement(pPr, _wTag("jc"), attrib={_wTag("val"): align})
rPr = ET.SubElement(xStyl, _wTag("rPr")) rPr = xmlSubElem(xStyl, _wTag("rPr"))
ET.SubElement(rPr, _wTag("sz"), attrib={_wTag("val"): sz}) xmlSubElem(rPr, _wTag("sz"), attrib={_wTag("val"): str(int(2.0 * size))})
ET.SubElement(rPr, _wTag("szCs"), attrib={_wTag("val"): sz}) xmlSubElem(rPr, _wTag("szCs"), attrib={_wTag("val"): str(int(2.0 * size))})
if color: if style.color:
ET.SubElement(rPr, _wTag("color"), attrib={_wTag("val"): color}) xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): style.color})
self._styles[style.styleId] = style
return return
@@ -698,10 +718,10 @@ class DocXParagraph:
def __init__(self) -> None: def __init__(self) -> None:
self._content: list[ET.Element] = [] self._content: list[ET.Element] = []
self._style: str = S_NORM self._style: DocXParStyle | None = None
self._textAlign: str | None = None self._textAlign: str | None = None
self._topMargin: int | None = None self._topMargin: float | None = None
self._bottomMargin: int | None = None self._bottomMargin: float | None = None
self._breakBefore = False self._breakBefore = False
self._breakAfter = False self._breakAfter = False
return return
@@ -710,9 +730,9 @@ class DocXParagraph:
# Setters # Setters
## ##
def setStyle(self, value: str) -> None: def setStyle(self, style: DocXParStyle | None) -> None:
"""Set the paragraph style.""" """Set the paragraph style."""
self._style = value self._style = style
return return
def setAlignment(self, value: str) -> None: def setAlignment(self, value: str) -> None:
@@ -723,12 +743,12 @@ class DocXParagraph:
def setMarginTop(self, value: float) -> None: def setMarginTop(self, value: float) -> None:
"""Set margin above in pt.""" """Set margin above in pt."""
self._topMargin = int(20.0 * value) self._topMargin = value
return return
def setMarginBottom(self, value: float) -> None: def setMarginBottom(self, value: float) -> None:
"""Set margin below in pt.""" """Set margin below in pt."""
self._bottomMargin = int(20.0 * value) self._bottomMargin = value
return return
def setPageBreakBefore(self, state: bool) -> None: def setPageBreakBefore(self, state: bool) -> None:
@@ -752,29 +772,36 @@ class DocXParagraph:
def finalise(self, body: ET.Element) -> None: def finalise(self, body: ET.Element) -> None:
"""Called after all content is set.""" """Called after all content is set."""
par = ET.SubElement(body, _wTag("p")) if style := self._style:
par = xmlSubElem(body, _wTag("p"))
# Values # Values
spacing = {} spacing = {}
if self._topMargin is not None: if self._topMargin is not None:
spacing["before"] = str(self._topMargin) spacing["before"] = str(self._topMargin)
if self._bottomMargin is not None: if self._bottomMargin is not None:
spacing["after"] = str(self._bottomMargin) spacing["after"] = str(self._bottomMargin)
# Paragraph # Paragraph
pPr = ET.SubElement(par, _wTag("pPr")) pPr = xmlSubElem(par, _wTag("pPr"))
_addSingle(pPr, _wTag("pStyle"), attrib={_wTag("val"): self._style}) xmlSubElem(pPr, _wTag("pStyle"), attrib={_wTag("val"): style.styleId})
if spacing: if self._topMargin is not None or self._bottomMargin is not None:
_addSingle(pPr, _wTag("spacing"), attrib=spacing) xmlSubElem(pPr, _wTag("spacing"), attrib={
if self._textAlign: _wTag("before"): str(int(20.0 * firstFloat(self._topMargin, style.before))),
_addSingle(pPr, _wTag("jc"), attrib={_wTag("val"): self._textAlign}) _wTag("after"): str(int(20.0 * firstFloat(self._bottomMargin, style.after))),
_wTag("line"): str(int(20.0 * firstFloat(style.line, style.size))),
})
if self._textAlign:
xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): self._textAlign})
# Text # Text
if self._breakBefore: if self._breakBefore:
_addSingle(ET.SubElement(par, _wTag("r")), _wTag("br"), attrib={_wTag("type"): "page"}) wr = xmlSubElem(par, _wTag("r"))
for run in self._content: xmlSubElem(wr, _wTag("br"), attrib={_wTag("type"): "page"})
par.append(run) for run in self._content:
if self._breakAfter: par.append(run)
_addSingle(ET.SubElement(par, _wTag("r")), _wTag("br"), attrib={_wTag("type"): "page"}) if self._breakAfter:
wr = xmlSubElem(par, _wTag("r"))
xmlSubElem(wr, _wTag("br"), attrib={_wTag("type"): "page"})
return return