Add structure for DocX documents

This commit is contained in:
Veronica Berglyd Olsen
2024-10-18 16:37:04 +02:00
parent 464454e7a6
commit 80d2dfd9a4
4 changed files with 234 additions and 7 deletions
+216
View File
@@ -0,0 +1,216 @@
"""
novelWriter DOCX Text Converter
=================================
File History:
Created: 2024-10-15 [2.6b1] ToRaw
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
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/>.
"""
from __future__ import annotations
import logging
import xml.etree.ElementTree as ET
from datetime import datetime
from pathlib import Path
from zipfile import ZipFile
from novelwriter import __version__
from novelwriter.common import xmlIndent
from novelwriter.core.project import NWProject
from novelwriter.formats.tokenizer import Tokenizer
logger = logging.getLogger(__name__)
# Types and Relationships
WORD_BASE = "application/vnd.openxmlformats-officedocument"
RELS_TYPE = "application/vnd.openxmlformats-package.relationships+xml"
REL_CORE = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
REL_APP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
REL_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
# Main XML NameSpaces
PROPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
XML_NS = {
"cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
"dc": "http://purl.org/dc/elements/1.1/",
"dcterms": "http://purl.org/dc/terms/",
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
for ns, uri in XML_NS.items():
ET.register_namespace(ns, uri)
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 _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
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)
# XML
self._dDoc = ET.Element("") # document.xml
self._xBody = ET.Element("") # Text body
# Internal
self._dLanguage = "en-GB"
return
##
# Setters
##
def setLanguage(self, language: str | None) -> None:
"""Set language for the document."""
if language:
self._dLanguage = language.replace("_", "-")
return
##
# Class Methods
##
def initDocument(self) -> None:
"""Initialises the DocX document structure."""
self._dDoc = ET.Element(_mkTag("w", "document"))
self._xBody = ET.SubElement(self._dDoc, _mkTag("w", "body"))
return
def doConvert(self) -> None:
"""Convert the list of text tokens into XML elements."""
self._result = "" # Not used, but cleared just in case
return
def saveDocument(self, path: Path) -> None:
"""Save the data to a .docx file."""
timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
# .rels
dRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS})
_addSingle(dRels, "Relationship", attrib={
"Id": "rId1", "Type": REL_CORE, "Target": "docProps/core.xml",
})
_addSingle(dRels, "Relationship", attrib={
"Id": "rId2", "Type": REL_APP, "Target": "docProps/app.xml",
})
_addSingle(dRels, "Relationship", attrib={
"Id": "rId3", "Type": REL_DOC, "Target": "word/document.xml",
})
# core.xml
dCore = ET.Element("coreProperties")
tsAttr = {_mkTag("xsi", "type"): "dcterms:W3CDTF"}
_addSingle(dCore, _mkTag("dcterms", "created"), timeStamp, attrib=tsAttr)
_addSingle(dCore, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr)
_addSingle(dCore, _mkTag("dc", "creator"), self._project.data.author)
_addSingle(dCore, _mkTag("dc", "title"), self._project.data.name)
_addSingle(dCore, _mkTag("dc", "creator"), self._project.data.author)
_addSingle(dCore, _mkTag("dc", "language"), self._dLanguage)
_addSingle(dCore, _mkTag("cp", "revision"), str(self._project.data.saveCount))
_addSingle(dCore, _mkTag("cp", "lastModifiedBy"), self._project.data.author)
# app.xml
dApp = ET.Element("Properties", attrib={"xmlns": PROPS_NS})
_addSingle(dApp, "TotalTime", self._project.data.editTime // 60)
_addSingle(dApp, "Application", f"novelWriter/{__version__}")
if count := self._counts.get("allWords"):
_addSingle(dApp, "Words", count)
if count := self._counts.get("textWordChars"):
_addSingle(dApp, "Characters", count)
if count := self._counts.get("textChars"):
_addSingle(dApp, "CharactersWithSpaces", count)
if count := self._counts.get("paragraphCount"):
_addSingle(dApp, "Paragraphs", count)
# document.xml.rels
dDRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS})
# [Content_Types].xml
dCont = ET.Element("Types", attrib={"xmlns": TYPES_NS})
_addSingle(dCont, "Default", attrib={
"Extension": "xml", "ContentType": "application/xml",
})
_addSingle(dCont, "Default", attrib={
"Extension": "rels", "ContentType": RELS_TYPE,
})
_addSingle(dCont, "Override", attrib={
"PartName": "/_rels/.rels",
"ContentType": RELS_TYPE,
})
_addSingle(dCont, "Override", attrib={
"PartName": "/docProps/core.xml",
"ContentType": f"{WORD_BASE}.extended-properties+xml",
})
_addSingle(dCont, "Override", attrib={
"PartName": "/docProps/app.xml",
"ContentType": "application/vnd.openxmlformats-package.core-properties+xml",
})
_addSingle(dCont, "Override", attrib={
"PartName": "/word/_rels/document.xml.rels",
"ContentType": RELS_TYPE,
})
_addSingle(dCont, "Override", attrib={
"PartName": "/word/document.xml",
"ContentType": f"{WORD_BASE}.wordprocessingml.document.main+xml",
})
def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
with zipObj.open(name, mode="w") as fObj:
xml = ET.ElementTree(xObj)
xmlIndent(xml)
xml.write(fObj, encoding="utf-8", xml_declaration=True)
with ZipFile(path, mode="w") as outZip:
xmlToZip("_rels/.rels", dRels, outZip)
xmlToZip("docProps/core.xml", dCore, outZip)
xmlToZip("docProps/app.xml", dApp, outZip)
xmlToZip("word/_rels/document.xml.rels", dDRels, outZip)
xmlToZip("word/document.xml", self._dDoc, outZip)
xmlToZip("[Content_Types].xml", dCont, outZip)
return