Add footnote support to DocX

This commit is contained in:
Veronica Berglyd Olsen
2024-10-20 22:01:13 +02:00
parent e9220c5e77
commit 7e1a676e0a
+72 -8
View File
@@ -48,7 +48,7 @@ RX_TEXT = re.compile(r"([\n\t])", re.UNICODE)
# Types and Relationships
OOXML_SCM = "http://schemas.openxmlformats.org"
WORD_BASE = "application/vnd.openxmlformats-officedocument"
WORD_BASE = "application/vnd.openxmlformats-officedocument.wordprocessingml"
RELS_TYPE = "application/vnd.openxmlformats-package.relationships+xml"
RELS_BASE = f"{OOXML_SCM}/officeDocument/2006/relationships"
@@ -108,9 +108,10 @@ S_HEAD1 = "Heading1"
S_HEAD2 = "Heading2"
S_HEAD3 = "Heading3"
S_HEAD4 = "Heading4"
S_HEAD = "Header"
S_SEP = "Separator"
S_META = "TextMeta"
S_HEAD = "Header"
S_FNOTE = "FootnoteText"
# Colours
COL_HEAD_L12 = "2a6099"
@@ -141,6 +142,7 @@ class DocXParStyle(NamedTuple):
after: float | None = None
line: float | None = None
indentFirst: float | None = None
indentHangning: float | None = None
align: str | None = None
default: bool = False
level: int | None = None
@@ -172,6 +174,7 @@ class ToDocX(Tokenizer):
self._pars: list[DocXParagraph] = []
self._files: dict[str, DocXXmlFile] = {}
self._styles: dict[str, DocXParStyle] = {}
self._usedNotes: list[str] = []
return
@@ -314,6 +317,9 @@ class ToDocX(Tokenizer):
self._documentXml(fId, dId)
if self._usedNotes:
self._footnotesXml()
return
def saveDocument(self, path: Path) -> None:
@@ -410,9 +416,14 @@ class ToDocX(Tokenizer):
"""Apply formatting tags to text."""
par.setStyle(self._styles.get(pStyle))
xFmt = 0x00
xNode = None
fStart = 0
for fPos, fFmt, fData in tFmt or []:
if xNode is not None:
par.addContent(xNode)
xNode = None
par.addContent(self._textRunToXml(text[fStart:fPos], xFmt))
if fFmt == self.FMT_B_B:
@@ -451,14 +462,17 @@ class ToDocX(Tokenizer):
xFmt |= X_DLA
elif fFmt == self.FMT_ADL_E:
xFmt &= M_DLA
# elif fmt == self.FMT_FNOTE:
# xNode = self._generateFootnote(fData)
elif fFmt == self.FMT_FNOTE:
xNode = self._generateFootnote(fData)
elif fFmt == self.FMT_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))
@@ -505,6 +519,18 @@ class ToDocX(Tokenizer):
# DocX Content
##
def _generateFootnote(self, key: str) -> ET.Element | None:
"""Generate a footnote XML object."""
if self._footnotes.get(key):
idx = len(self._usedNotes)
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)})
self._usedNotes.append(key)
return run
return None
def _generateStyles(self) -> None:
"""Generate usable styles."""
styles: list[DocXParStyle] = []
@@ -512,6 +538,7 @@ class ToDocX(Tokenizer):
hScale = self._scaleHeads
hColor = self._colorHeads
fSz = self._fontSize
fnSz = 0.8 * 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
@@ -641,6 +668,18 @@ class ToDocX(Tokenizer):
align="right",
))
# Footnote
styles.append(DocXParStyle(
name="Footnote Text",
styleId=S_FNOTE,
size=fnSz,
basedOn=S_NORM,
before=0.0,
after=fnSz * self._marginFoot[1],
line=fnSz * self._lineHeight,
indentHangning=fnSz * self._marginFoot[0],
))
# Add to Cache
for style in styles:
self._styles[style.styleId] = style
@@ -712,7 +751,7 @@ class ToDocX(Tokenizer):
rId=rId,
path="word",
relType=f"{RELS_BASE}/styles",
contentType=f"{WORD_BASE}.wordprocessingml.styles+xml",
contentType=f"{WORD_BASE}.styles+xml",
)
# Default Style
@@ -759,6 +798,11 @@ class ToDocX(Tokenizer):
_wTag("after"): str(int(20.0 * firstFloat(style.after))),
_wTag("line"): str(int(20.0 * firstFloat(style.line, size))),
})
if style.indentHangning is not None:
xmlSubElem(pPr, _wTag("ind"), attrib={
_wTag("left"): str(int(20.0 * style.indentHangning)),
_wTag("hanging"): str(int(20.0 * style.indentHangning)),
})
if style.align:
xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): style.align})
if style.level is not None:
@@ -784,7 +828,7 @@ class ToDocX(Tokenizer):
rId=rId,
path="word",
relType=f"{RELS_BASE}/header",
contentType=f"{WORD_BASE}.wordprocessingml.header+xml",
contentType=f"{WORD_BASE}.header+xml",
)
xP = xmlSubElem(xRoot, _wTag("p"))
@@ -830,7 +874,7 @@ class ToDocX(Tokenizer):
rId=rId,
path="word",
relType=f"{RELS_BASE}/header",
contentType=f"{WORD_BASE}.wordprocessingml.header+xml",
contentType=f"{WORD_BASE}.header+xml",
)
xP = xmlSubElem(xRoot, _wTag("p"))
@@ -854,7 +898,7 @@ class ToDocX(Tokenizer):
rId=rId,
path="word",
relType=f"{RELS_BASE}/officeDocument",
contentType=f"{WORD_BASE}.wordprocessingml.document.main+xml",
contentType=f"{WORD_BASE}.document.main+xml",
)
# Map all Page Break Before to After where possible
@@ -918,6 +962,26 @@ class ToDocX(Tokenizer):
return rId
def _footnotesXml(self) -> str:
"""Populate footnotes.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("footnotes"))
self._files["footnotes.xml"] = DocXXmlFile(
xml=xRoot,
rId=rId,
path="word",
relType=f"{RELS_BASE}/footnotes",
contentType=f"{WORD_BASE}.footnotes+xml",
)
for i, key in enumerate(self._usedNotes):
par = DocXParagraph()
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(i)}))
return rId
class DocXParagraph: