Add font table, fix core.xml bug, clean up code, update tests, for DocX

This commit is contained in:
Veronica Berglyd Olsen
2024-10-26 17:09:50 +02:00
parent 6c217c2450
commit ceabc666c3
14 changed files with 163 additions and 100 deletions
+121 -71
View File
@@ -87,6 +87,17 @@ def _docXCol(color: QColor) -> str:
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)
# Cached
W_VAL = _wTag("val")
# Formatting Codes
X_BLD = 0x001 # Bold format
X_ITA = 0x002 # Italic format
@@ -145,9 +156,9 @@ class DocXParStyle(NamedTuple):
nextStyle: str | None = None
before: float | None = None
after: float | None = None
left: float | None = None
line: float | None = None
indentFirst: float | None = None
hanging: float | None = None
align: str | None = None
default: bool = False
level: int | None = None
@@ -191,7 +202,7 @@ class ToDocX(Tokenizer):
def setLanguage(self, language: str | None) -> None:
"""Set language for the document."""
if language:
self._dLanguage = language
self._dLanguage = language.replace("_", "-")
return
def setPageLayout(
@@ -296,6 +307,7 @@ class ToDocX(Tokenizer):
self._coreXml()
self._appXml()
self._stylesXml()
self._fontTableXml()
fId = None
dId = None
@@ -451,32 +463,30 @@ class ToDocX(Tokenizer):
if fmt & X_ITA:
xmlSubElem(rPr, _wTag("i"))
if fmt & X_UND:
xmlSubElem(rPr, _wTag("u"), attrib={_wTag("val"): "single"})
xmlSubElem(rPr, _wTag("u"), attrib={W_VAL: "single"})
if fmt & X_MRK:
xmlSubElem(rPr, _wTag("shd"), attrib={
_wTag("fill"): _docXCol(self._theme.highlight), _wTag("val"): "clear",
_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={_wTag("val"): "superscript"})
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
if fmt & X_SUB:
xmlSubElem(rPr, _wTag("vertAlign"), attrib={_wTag("val"): "subscript"})
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "subscript"})
if fmt & X_COL and (color := self._classes.get(fClass)):
xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): _docXCol(color)})
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
for segment in RX_TEXT.split(text):
if segment == "\n":
xmlSubElem(xR, _wTag("br"))
elif segment == "\t":
xmlSubElem(xR, _wTag("tab"))
elif len(segment) != len(segment.strip()):
xmlSubElem(xR, _wTag("t"), segment, attrib={_mkTag("xml", "space"): "preserve"})
elif segment:
xmlSubElem(xR, _wTag("t"), segment)
_wText(xR, segment)
if fmt & X_HRF and fLink:
xmlSubElem(rPr, _wTag("rStyle"), attrib={_wTag("val"): "InternetLink"})
xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
rId = self._appendExternalRel(fLink)
xH = ET.Element(_wTag("hyperlink"), attrib={_mkTag("r", "id"): rId})
xH.append(xR)
@@ -492,12 +502,12 @@ class ToDocX(Tokenizer):
"""Generate a footnote XML object."""
if key in self._footnotes:
idx = len(self._usedNotes) + 1
run = ET.Element(_wTag("r"))
rPr = xmlSubElem(run, _wTag("rPr"))
xmlSubElem(rPr, _wTag("vertAlign"), attrib={_wTag("val"): "superscript"})
xmlSubElem(run, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
xR = ET.Element(_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 run
return xR
return None
def _generateStyles(self) -> None:
@@ -643,8 +653,8 @@ class ToDocX(Tokenizer):
basedOn=S_NORM,
before=0.0,
after=fnSz * self._marginFoot[1],
left=fnSz * self._marginFoot[0],
line=fnSz * self._lineHeight,
hanging=fnSz * self._marginFoot[0],
))
# Add to Cache
@@ -701,7 +711,7 @@ class ToDocX(Tokenizer):
def _coreXml(self) -> str:
"""Populate app.xml."""
rId = self._nextRelId()
xRoot = ET.Element("coreProperties")
xRoot = ET.Element(_mkTag("cp", "coreProperties"))
self._rels["core.xml"] = DocXXmlRel(
rId=rId,
relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
@@ -718,7 +728,6 @@ class ToDocX(Tokenizer):
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", "creator"), self._project.data.author)
xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLanguage)
xmlSubElem(xRoot, _mkTag("cp", "revision"), str(self._project.data.saveCount))
xmlSubElem(xRoot, _mkTag("cp", "lastModifiedBy"), self._project.data.author)
@@ -754,9 +763,9 @@ class ToDocX(Tokenizer):
_wTag("hAnsi"): self._fontFamily,
_wTag("cs"): self._fontFamily,
})
xmlSubElem(xRPr, _wTag("sz"), attrib={_wTag("val"): size})
xmlSubElem(xRPr, _wTag("szCs"), attrib={_wTag("val"): size})
xmlSubElem(xRPr, _wTag("lang"), attrib={_wTag("val"): self._dLanguage})
xmlSubElem(xRPr, _wTag("sz"), attrib={W_VAL: size})
xmlSubElem(xRPr, _wTag("szCs"), attrib={W_VAL: size})
xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLanguage})
xmlSubElem(xPPr, _wTag("spacing"), attrib={_wTag("line"): line})
# Paragraph Styles
@@ -770,11 +779,11 @@ class ToDocX(Tokenizer):
size = firstFloat(style.size, self._fontSize)
xStyl = xmlSubElem(xRoot, _wTag("style"), attrib=sAttr)
xmlSubElem(xStyl, _wTag("name"), attrib={_wTag("val"): style.name})
xmlSubElem(xStyl, _wTag("name"), attrib={W_VAL: style.name})
if style.basedOn:
xmlSubElem(xStyl, _wTag("basedOn"), attrib={_wTag("val"): style.basedOn})
xmlSubElem(xStyl, _wTag("basedOn"), attrib={W_VAL: style.basedOn})
if style.nextStyle:
xmlSubElem(xStyl, _wTag("next"), attrib={_wTag("val"): style.nextStyle})
xmlSubElem(xStyl, _wTag("next"), attrib={W_VAL: style.nextStyle})
# pPr Node
pPr = xmlSubElem(xStyl, _wTag("pPr"))
@@ -783,34 +792,33 @@ class ToDocX(Tokenizer):
_wTag("after"): str(int(20.0 * firstFloat(style.after))),
_wTag("line"): str(int(20.0 * firstFloat(style.line, size))),
})
if style.hanging is not None:
if style.left is not None:
xmlSubElem(pPr, _wTag("ind"), attrib={
_wTag("left"): str(int(20.0 * style.hanging)),
_wTag("hanging"): str(int(20.0 * style.hanging)),
_wTag("left"): str(int(20.0 * style.left)),
})
if style.align:
xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): style.align})
xmlSubElem(pPr, _wTag("jc"), attrib={W_VAL: style.align})
if style.level is not None:
xmlSubElem(pPr, _wTag("outlineLvl"), attrib={_wTag("val"): str(style.level)})
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={_wTag("val"): style.color})
xmlSubElem(rPr, _wTag("sz"), attrib={_wTag("val"): str(int(2.0 * size))})
xmlSubElem(rPr, _wTag("szCs"), attrib={_wTag("val"): str(int(2.0 * size))})
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={_wTag("val"): "Hyperlink"})
xmlSubElem(xStyl, _wTag("name"), attrib={W_VAL: "Hyperlink"})
rPr = xmlSubElem(xStyl, _wTag("rPr"))
xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): _docXCol(self._theme.link)})
xmlSubElem(rPr, _wTag("u"), attrib={_wTag("val"): "single"})
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(self._theme.link)})
xmlSubElem(rPr, _wTag("u"), attrib={W_VAL: "single"})
return rId
@@ -830,8 +838,8 @@ class ToDocX(Tokenizer):
xP = xmlSubElem(xRoot, _wTag("p"))
xPPr = xmlSubElem(xP, _wTag("pPr"))
xmlSubElem(xPPr, _wTag("pStyle"), attrib={_wTag("val"): S_HEAD})
xmlSubElem(xPPr, _wTag("jc"), attrib={_wTag("val"): "right"})
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)
@@ -840,25 +848,24 @@ class ToDocX(Tokenizer):
post = post.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
post = post.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
xSpace = _mkTag("xml", "space")
wFldCT = _wTag("fldCharType")
parts: list[tuple[str, str | None, str, str]] = []
if pre:
parts.append(("t", pre, xSpace, "preserve"))
if page:
parts.append(("fldChar", None, wFldCT, "begin"))
parts.append(("t", " PAGE ", xSpace, "preserve"))
parts.append(("fldChar", None, wFldCT, "separate"))
parts.append(("t", "2", xSpace, "preserve"))
parts.append(("fldChar", None, wFldCT, "end"))
if post:
parts.append(("t", post, xSpace, "preserve"))
for part in parts:
xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("rPr"))
xmlSubElem(xR, _wTag(part[0]), part[1], attrib={part[2]: part[3]})
_wText(xR, pre)
if page:
xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "begin"})
xR = xmlSubElem(xP, _wTag("r"))
_wText(xR, " PAGE ")
xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "separate"})
xR = xmlSubElem(xP, _wTag("r"))
_wText(xR, "0")
xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("fldChar"), attrib={wFldCT: "end"})
if post:
xR = xmlSubElem(xP, _wTag("r"))
_wText(xR, post)
return rId
@@ -878,8 +885,8 @@ class ToDocX(Tokenizer):
xP = xmlSubElem(xRoot, _wTag("p"))
xPPr = xmlSubElem(xP, _wTag("pPr"))
xmlSubElem(xPPr, _wTag("pStyle"), attrib={_wTag("val"): S_HEAD})
xmlSubElem(xPPr, _wTag("jc"), attrib={_wTag("val"): "right"})
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"))
@@ -891,6 +898,7 @@ class ToDocX(Tokenizer):
"""Populate document.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("document"))
xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml")
xBody = xmlSubElem(xRoot, _wTag("body"))
self._rels["document.xml"] = DocXXmlRel(
rId=rId,
@@ -931,7 +939,7 @@ class ToDocX(Tokenizer):
xFn = xmlSubElem(xSect, _wTag("footnotePr"))
xmlSubElem(xFn, _wTag("numFmt"), attrib={
_wTag("val"): "decimal",
W_VAL: "decimal",
})
xmlSubElem(xSect, _wTag("pgSz"), attrib={
@@ -972,12 +980,36 @@ class ToDocX(Tokenizer):
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 = ET.Element(_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()
@@ -992,15 +1024,22 @@ class ToDocX(Tokenizer):
contentType=f"{WORD_BASE}.settings+xml",
)
xFn = xmlSubElem(xRoot, _wTag("footnotePr"))
xmlSubElem(xFn, _wTag("numFmt"), attrib={_wTag("val"): "decimal"})
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:]}",
_wTag("val"): str(value),
W_VAL: str(value),
})
return rId
@@ -1011,7 +1050,7 @@ class DocXParagraph:
__slots__ = (
"_content", "_style", "_textAlign",
"_topMargin", "_bottomMargin", "_leftMargin", "_rightMargin",
"_indentFirst", "_breakBefore", "_breakAfter",
"_indentFirst", "_breakBefore", "_breakAfter", "_footnoteRef",
)
def __init__(self) -> None:
@@ -1025,6 +1064,7 @@ class DocXParagraph:
self._indentFirst = False
self._breakBefore = False
self._breakAfter = False
self._footnoteRef = False
return
##
@@ -1086,6 +1126,11 @@ class DocXParagraph:
self._breakAfter = state
return
def setIsFootnote(self, state: bool) -> None:
"""Set is footnote flag."""
self._footnoteRef = state
return
##
# Methods
##
@@ -1098,7 +1143,7 @@ class DocXParagraph:
def toXml(self, body: ET.Element) -> None:
"""Called after all content is set."""
if style := self._style:
par = xmlSubElem(body, _wTag("p"))
xP = xmlSubElem(body, _wTag("p"))
# Values
indent = {}
@@ -1110,8 +1155,8 @@ class DocXParagraph:
indent[_wTag("right")] = str(int(20.0 * self._rightMargin))
# Paragraph
pPr = xmlSubElem(par, _wTag("pPr"))
xmlSubElem(pPr, _wTag("pStyle"), attrib={_wTag("val"): style.styleId})
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))),
@@ -1121,16 +1166,21 @@ class DocXParagraph:
if indent:
xmlSubElem(pPr, _wTag("ind"), attrib=indent)
if self._textAlign:
xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): 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:
wr = xmlSubElem(par, _wTag("r"))
xmlSubElem(wr, _wTag("br"), attrib={_wTag("type"): "page"})
for run in self._content:
par.append(run)
xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
for xR in self._content:
xP.append(xR)
if self._breakAfter:
wr = xmlSubElem(par, _wTag("r"))
xmlSubElem(wr, _wTag("br"), attrib={_wTag("type"): "page"})
xR = xmlSubElem(xP, _wTag("r"))
xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
return