1c45331b0a
* Fix bug in early error reporting in main init * Update docstrings and optimise code in GuiMain * Update docstrings and optimise code in Config * Update docstrings and optimise code in common module * Update docstrings and optimise code in main project classes * Update the OptionState class * Update the spell checker class * Update the file converter classes and extend tests * Update the about, merge, split and item editor classes and extend tests * Update item editor test * Update about dialog tests * Some minor test cleanup * Fix typo and add clarification in contributing guide
1487 lines
50 KiB
Python
1487 lines
50 KiB
Python
"""
|
||
novelWriter – ODT Text Converter
|
||
================================
|
||
Extends the Tokenizer class to generate ODT and FODT files
|
||
|
||
File History:
|
||
Created: 2021-01-26 [1.2a0]
|
||
|
||
This file is a part of novelWriter
|
||
Copyright 2018–2021, 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/>.
|
||
"""
|
||
|
||
import logging
|
||
import novelwriter
|
||
|
||
from lxml import etree
|
||
from hashlib import sha256
|
||
from zipfile import ZipFile
|
||
from datetime import datetime
|
||
|
||
from novelwriter.constants import nwKeyWords, nwLabels
|
||
from novelwriter.core.tokenizer import Tokenizer
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Main XML NameSpaces
|
||
XML_NS = {
|
||
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
||
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
||
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
|
||
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
||
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
|
||
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
||
}
|
||
MANI_NS = {
|
||
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
|
||
}
|
||
OFFICE_NS = {
|
||
"office": XML_NS["office"]
|
||
}
|
||
|
||
|
||
def _mkTag(nsName, tagName, nsMap=XML_NS):
|
||
"""Assemble namespace and tag name.
|
||
"""
|
||
theNS = nsMap.get(nsName, "")
|
||
if theNS:
|
||
return f"{{{theNS}}}{tagName}"
|
||
logger.warning("Missing xml namespace '%s'", nsName)
|
||
return tagName
|
||
|
||
|
||
# Mimetype and Version
|
||
X_MIME = "application/vnd.oasis.opendocument.text"
|
||
X_VERS = "1.2"
|
||
|
||
# Text Formatting Tags
|
||
TAG_BR = _mkTag("text", "line-break")
|
||
TAG_SPC = _mkTag("text", "s")
|
||
TAG_NSPC = _mkTag("text", "c")
|
||
TAG_TAB = _mkTag("text", "tab")
|
||
TAG_SPAN = _mkTag("text", "span")
|
||
TAG_STNM = _mkTag("text", "style-name")
|
||
|
||
# Formatting Codes
|
||
X_BLD = 0x01 # Bold format
|
||
X_ITA = 0x02 # Italic format
|
||
X_DEL = 0x04 # Strikethrough format
|
||
|
||
# Formatting Masks
|
||
M_BLD = ~X_BLD
|
||
M_ITA = ~X_ITA
|
||
M_DEL = ~X_DEL
|
||
|
||
|
||
class ToOdt(Tokenizer):
|
||
|
||
def __init__(self, theProject, isFlat):
|
||
Tokenizer.__init__(self, theProject)
|
||
|
||
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
||
|
||
self._dFlat = None # FODT file XML root
|
||
self._dCont = None # ODT content.xml root
|
||
self._dMeta = None # ODT meta.xml root
|
||
self._dStyl = None # ODT styles.xml root
|
||
|
||
self._xMeta = None # Office meta root
|
||
self._xFont = None # Office font face declaration
|
||
self._xFnt2 = None # Office font face declaration, secondary
|
||
self._xStyl = None # Office styles root
|
||
self._xAuto = None # Office auto-styles root
|
||
self._xAut2 = None # Office auto-styles root, secondary
|
||
self._xMast = None # Office master-styles root
|
||
self._xBody = None # Office body root
|
||
self._xText = None # Office text root
|
||
|
||
self._mainPara = {} # User-accessible paragraph styles
|
||
self._autoPara = {} # Auto-generated paragraph styles
|
||
self._autoText = {} # Auto-generated text styles
|
||
|
||
self._errData = [] # List of errors encountered
|
||
|
||
# Properties
|
||
self.textFont = "Liberation Serif"
|
||
self.textSize = 12
|
||
self.textFixed = False
|
||
self.colourHead = False
|
||
self.headerText = ""
|
||
|
||
# Internal
|
||
self._fontFamily = "'Liberation Serif'"
|
||
self._fontPitch = "variable"
|
||
self._fSizeTitle = "30pt"
|
||
self._fSizeHead1 = "24pt"
|
||
self._fSizeHead2 = "20pt"
|
||
self._fSizeHead3 = "16pt"
|
||
self._fSizeHead4 = "14pt"
|
||
self._fSizeHead = "14pt"
|
||
self._fSizeText = "12pt"
|
||
self._lineHeight = "115%"
|
||
self._blockIndent = "1.693cm"
|
||
self._textAlign = "left"
|
||
self._dLanguage = "en"
|
||
self._dCountry = "GB"
|
||
|
||
# Text Margings in Units of em
|
||
self._mTopTitle = "0.423cm"
|
||
self._mTopHead1 = "0.423cm"
|
||
self._mTopHead2 = "0.353cm"
|
||
self._mTopHead3 = "0.247cm"
|
||
self._mTopHead4 = "0.247cm"
|
||
self._mTopHead = "0.423cm"
|
||
self._mTopText = "0.000cm"
|
||
self._mTopMeta = "0.000cm"
|
||
|
||
self._mBotTitle = "0.212cm"
|
||
self._mBotHead1 = "0.212cm"
|
||
self._mBotHead2 = "0.212cm"
|
||
self._mBotHead3 = "0.212cm"
|
||
self._mBotHead4 = "0.212cm"
|
||
self._mBotHead = "0.212cm"
|
||
self._mBotText = "0.247cm"
|
||
self._mBotMeta = "0.106cm"
|
||
|
||
# Document Margins
|
||
self._mDocTop = "2.000cm"
|
||
self._mDocBtm = "2.000cm"
|
||
self._mDocLeft = "2.000cm"
|
||
self._mDocRight = "2.000cm"
|
||
|
||
# Colour
|
||
self._colHead12 = None
|
||
self._opaHead12 = None
|
||
self._colHead34 = None
|
||
self._opaHead34 = None
|
||
self._colMetaTx = None
|
||
self._opaMetaTx = None
|
||
|
||
return
|
||
|
||
##
|
||
# Setters
|
||
##
|
||
|
||
def setLanguage(self, theLang):
|
||
"""Set language for the document.
|
||
"""
|
||
if theLang is None:
|
||
return False
|
||
|
||
langBits = theLang.split("_")
|
||
self._dLanguage = langBits[0]
|
||
if len(langBits) > 1:
|
||
self._dCountry = langBits[1]
|
||
|
||
return True
|
||
|
||
def setColourHeaders(self, doColour):
|
||
"""Enable/disable coloured headings and comments.
|
||
"""
|
||
self.colourHead = doColour
|
||
return
|
||
|
||
##
|
||
# Class Methods
|
||
##
|
||
|
||
def getErrors(self):
|
||
"""Return the list of errors."""
|
||
return self._errData
|
||
|
||
def initDocument(self):
|
||
"""Initialises a new open document XML tree.
|
||
"""
|
||
# Initialise Variables
|
||
# ====================
|
||
|
||
self._fontFamily = self.textFont
|
||
if len(self.textFont.split()) > 1:
|
||
self._fontFamily = f"'{self.textFont}'"
|
||
self._fontPitch = "fixed" if self.textFixed else "variable"
|
||
|
||
self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt"
|
||
self._fSizeHead1 = f"{round(2.00 * self.textSize):d}pt"
|
||
self._fSizeHead2 = f"{round(1.60 * self.textSize):d}pt"
|
||
self._fSizeHead3 = f"{round(1.30 * self.textSize):d}pt"
|
||
self._fSizeHead4 = f"{round(1.15 * self.textSize):d}pt"
|
||
self._fSizeHead = f"{round(1.15 * self.textSize):d}pt"
|
||
self._fSizeText = f"{self.textSize:d}pt"
|
||
|
||
mScale = self.lineHeight/1.15
|
||
|
||
self._mTopTitle = self._emToCm(mScale * self.marginTitle[0])
|
||
self._mTopHead1 = self._emToCm(mScale * self.marginHead1[0])
|
||
self._mTopHead2 = self._emToCm(mScale * self.marginHead2[0])
|
||
self._mTopHead3 = self._emToCm(mScale * self.marginHead3[0])
|
||
self._mTopHead4 = self._emToCm(mScale * self.marginHead4[0])
|
||
self._mTopHead = self._emToCm(mScale * self.marginHead4[0])
|
||
self._mTopText = self._emToCm(mScale * self.marginText[0])
|
||
self._mTopMeta = self._emToCm(mScale * self.marginMeta[0])
|
||
|
||
self._mBotTitle = self._emToCm(mScale * self.marginTitle[1])
|
||
self._mBotHead1 = self._emToCm(mScale * self.marginHead1[1])
|
||
self._mBotHead2 = self._emToCm(mScale * self.marginHead2[1])
|
||
self._mBotHead3 = self._emToCm(mScale * self.marginHead3[1])
|
||
self._mBotHead4 = self._emToCm(mScale * self.marginHead4[1])
|
||
self._mBotHead = self._emToCm(mScale * self.marginHead4[1])
|
||
self._mBotText = self._emToCm(mScale * self.marginText[1])
|
||
self._mBotMeta = self._emToCm(mScale * self.marginMeta[1])
|
||
|
||
if self.colourHead:
|
||
self._colHead12 = "#2a6099"
|
||
self._opaHead12 = "100%"
|
||
self._colHead34 = "#444444"
|
||
self._opaHead34 = "100%"
|
||
self._colMetaTx = "#813709"
|
||
self._opaMetaTx = "100%"
|
||
|
||
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
|
||
self._blockIndent = self._emToCm(self.blockIndent)
|
||
self._textAlign = "justify" if self.doJustify else "left"
|
||
|
||
# Clear Errors
|
||
self._errData = []
|
||
|
||
# Document Header
|
||
# ===============
|
||
|
||
if self.headerText == "":
|
||
theTitle = self.theProject.bookTitle
|
||
theAuth = self.theProject.getAuthors()
|
||
self.headerText = f"{theTitle} / {theAuth} /"
|
||
|
||
# Create Roots
|
||
# ============
|
||
|
||
tAttr = {}
|
||
tAttr[_mkTag("office", "version")] = X_VERS
|
||
|
||
fAttr = {}
|
||
fAttr[_mkTag("style", "name")] = self.textFont
|
||
fAttr[_mkTag("style", "font-pitch")] = self._fontPitch
|
||
|
||
if self._isFlat:
|
||
|
||
# FODT File
|
||
# =========
|
||
|
||
tAttr[_mkTag("office", "mimetype")] = X_MIME
|
||
|
||
tFlat = _mkTag("office", "document")
|
||
self._dFlat = etree.Element(tFlat, attrib=tAttr, nsmap=XML_NS)
|
||
|
||
self._xMeta = etree.SubElement(self._dFlat, _mkTag("office", "meta"))
|
||
self._xFont = etree.SubElement(self._dFlat, _mkTag("office", "font-face-decls"))
|
||
self._xStyl = etree.SubElement(self._dFlat, _mkTag("office", "styles"))
|
||
self._xAuto = etree.SubElement(self._dFlat, _mkTag("office", "automatic-styles"))
|
||
self._xMast = etree.SubElement(self._dFlat, _mkTag("office", "master-styles"))
|
||
self._xBody = etree.SubElement(self._dFlat, _mkTag("office", "body"))
|
||
|
||
etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
|
||
|
||
else:
|
||
|
||
# ODT File
|
||
# ========
|
||
|
||
tCont = _mkTag("office", "document-content")
|
||
tMeta = _mkTag("office", "document-meta")
|
||
tStyl = _mkTag("office", "document-styles")
|
||
|
||
# content.xml
|
||
self._dCont = etree.Element(tCont, attrib=tAttr, nsmap=XML_NS)
|
||
self._xFont = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls"))
|
||
self._xAuto = etree.SubElement(self._dCont, _mkTag("office", "automatic-styles"))
|
||
self._xBody = etree.SubElement(self._dCont, _mkTag("office", "body"))
|
||
|
||
# meta.xml
|
||
self._dMeta = etree.Element(tMeta, attrib=tAttr, nsmap=XML_NS)
|
||
self._xMeta = etree.SubElement(self._dMeta, _mkTag("office", "meta"))
|
||
|
||
# styles.xml
|
||
self._dStyl = etree.Element(tStyl, attrib=tAttr, nsmap=XML_NS)
|
||
self._xFnt2 = etree.SubElement(self._dStyl, _mkTag("office", "font-face-decls"))
|
||
self._xStyl = etree.SubElement(self._dStyl, _mkTag("office", "styles"))
|
||
self._xAut2 = etree.SubElement(self._dStyl, _mkTag("office", "automatic-styles"))
|
||
self._xMast = etree.SubElement(self._dStyl, _mkTag("office", "master-styles"))
|
||
|
||
etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
|
||
etree.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr)
|
||
|
||
# Finalise
|
||
# ========
|
||
|
||
self._xText = etree.SubElement(self._xBody, _mkTag("office", "text"))
|
||
|
||
# Meta Data
|
||
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
|
||
xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
|
||
|
||
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
|
||
xMeta.text = f"novelWriter/{novelwriter.__version__}"
|
||
|
||
self._pageStyles()
|
||
self._defaultStyles()
|
||
self._useableStyles()
|
||
self._writeHeader()
|
||
|
||
return
|
||
|
||
def doConvert(self):
|
||
"""Convert the list of text tokens into XML elements.
|
||
"""
|
||
self.theResult = "" # Not used, but cleared just in case
|
||
|
||
odtTags = {
|
||
self.FMT_B_B: "_B", # Bold open format
|
||
self.FMT_B_E: "b_", # Bold close format
|
||
self.FMT_I_B: "I", # Italic open format
|
||
self.FMT_I_E: "i", # Italic close format
|
||
self.FMT_D_B: "_S", # Strikethrough open format
|
||
self.FMT_D_E: "s_", # Strikethrough close format
|
||
}
|
||
|
||
thisPar = []
|
||
thisFmt = []
|
||
parStyle = None
|
||
for tType, _, tText, tFormat, tStyle in self.theTokens:
|
||
|
||
# Styles
|
||
oStyle = ODTParagraphStyle()
|
||
if tStyle is not None:
|
||
if tStyle & self.A_LEFT:
|
||
oStyle.setTextAlign("left")
|
||
elif tStyle & self.A_RIGHT:
|
||
oStyle.setTextAlign("right")
|
||
elif tStyle & self.A_CENTRE:
|
||
oStyle.setTextAlign("center")
|
||
elif tStyle & self.A_JUSTIFY:
|
||
oStyle.setTextAlign("justify")
|
||
|
||
if tStyle & self.A_PBB:
|
||
oStyle.setBreakBefore("page")
|
||
|
||
if tStyle & self.A_PBA:
|
||
oStyle.setBreakAfter("page")
|
||
|
||
if tStyle & self.A_Z_BTMMRG:
|
||
oStyle.setMarginBottom("0.000cm")
|
||
if tStyle & self.A_Z_TOPMRG:
|
||
oStyle.setMarginTop("0.000cm")
|
||
|
||
if tStyle & self.A_IND_L:
|
||
oStyle.setMarginLeft(self._blockIndent)
|
||
if tStyle & self.A_IND_R:
|
||
oStyle.setMarginRight(self._blockIndent)
|
||
|
||
# Process Text Types
|
||
if tType == self.T_EMPTY:
|
||
if len(thisPar) > 1 and parStyle is not None:
|
||
if self.doJustify:
|
||
parStyle.setTextAlign("left")
|
||
|
||
if len(thisPar) > 0:
|
||
tTemp = "\n".join(thisPar)
|
||
fTemp = " ".join(thisFmt)
|
||
tTxt = tTemp.rstrip()
|
||
tFmt = fTemp[:len(tTxt)]
|
||
self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
|
||
|
||
thisPar = []
|
||
thisFmt = []
|
||
parStyle = None
|
||
|
||
elif tType == self.T_TITLE:
|
||
tHead = tText.replace(r"\\", "\n")
|
||
self._addTextPar("Title", oStyle, tHead, isHead=True)
|
||
|
||
elif tType == self.T_UNNUM:
|
||
tHead = tText.replace(r"\\", "\n")
|
||
self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2")
|
||
|
||
elif tType == self.T_HEAD1:
|
||
tHead = tText.replace(r"\\", "\n")
|
||
self._addTextPar("Heading_1", oStyle, tHead, isHead=True, oLevel="1")
|
||
|
||
elif tType == self.T_HEAD2:
|
||
tHead = tText.replace(r"\\", "\n")
|
||
self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2")
|
||
|
||
elif tType == self.T_HEAD3:
|
||
tHead = tText.replace(r"\\", "\n")
|
||
self._addTextPar("Heading_3", oStyle, tHead, isHead=True, oLevel="3")
|
||
|
||
elif tType == self.T_HEAD4:
|
||
tHead = tText.replace(r"\\", "\n")
|
||
self._addTextPar("Heading_4", oStyle, tHead, isHead=True, oLevel="4")
|
||
|
||
elif tType == self.T_SEP:
|
||
self._addTextPar("Text_Body", oStyle, tText)
|
||
|
||
elif tType == self.T_SKIP:
|
||
self._addTextPar("Text_Body", oStyle, "")
|
||
|
||
elif tType == self.T_TEXT:
|
||
if parStyle is None:
|
||
parStyle = oStyle
|
||
|
||
tFmt = " "*len(tText)
|
||
for xPos, xLen, xFmt in tFormat:
|
||
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
|
||
|
||
tTxt = tText.rstrip()
|
||
tFmt = tFmt[:len(tTxt)]
|
||
thisPar.append(tTxt)
|
||
thisFmt.append(tFmt)
|
||
|
||
elif tType == self.T_SYNOPSIS and self.doSynopsis:
|
||
tTemp, fTemp = self._formatSynopsis(tText)
|
||
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
|
||
|
||
elif tType == self.T_COMMENT and self.doComments:
|
||
tTemp, fTemp = self._formatComments(tText)
|
||
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
|
||
|
||
elif tType == self.T_KEYWORD and self.doKeywords:
|
||
tTemp, fTemp = self._formatKeywords(tText)
|
||
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
|
||
|
||
return
|
||
|
||
def closeDocument(self):
|
||
"""Return the serialised XML document
|
||
"""
|
||
# Build the auto-generated styles
|
||
for styleName, styleObj in self._autoPara.values():
|
||
styleObj.packXML(self._xAuto, styleName)
|
||
for styleName, styleObj in self._autoText.values():
|
||
styleObj.packXML(self._xAuto, styleName)
|
||
|
||
return
|
||
|
||
def saveFlatXML(self, savePath):
|
||
"""Save the data to an .fodt file.
|
||
"""
|
||
with open(savePath, mode="wb") as outFile:
|
||
outFile.write(etree.tostring(
|
||
self._dFlat,
|
||
pretty_print=True,
|
||
encoding="utf-8",
|
||
xml_declaration=True
|
||
))
|
||
return
|
||
|
||
def saveOpenDocText(self, savePath):
|
||
"""Save the data to an .odt file.
|
||
"""
|
||
mMani = _mkTag("manifest", "manifest", nsMap=MANI_NS)
|
||
mVers = _mkTag("manifest", "version", nsMap=MANI_NS)
|
||
mPath = _mkTag("manifest", "full-path", nsMap=MANI_NS)
|
||
mType = _mkTag("manifest", "media-type", nsMap=MANI_NS)
|
||
mFile = _mkTag("manifest", "file-entry", nsMap=MANI_NS)
|
||
|
||
xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=MANI_NS)
|
||
etree.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
|
||
etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
|
||
etree.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
|
||
etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
|
||
etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
|
||
|
||
oRoot = _mkTag("office", "document-settings", nsMap=OFFICE_NS)
|
||
oSett = _mkTag("office", "settings", nsMap=OFFICE_NS)
|
||
xSett = etree.Element(oRoot, nsmap=OFFICE_NS)
|
||
etree.SubElement(xSett, oSett)
|
||
|
||
with ZipFile(savePath, mode="w") as outFile:
|
||
outFile.writestr("mimetype", X_MIME)
|
||
outFile.writestr("META-INF/manifest.xml", etree.tostring(
|
||
xMani, pretty_print=False, encoding="utf-8", xml_declaration=True
|
||
))
|
||
outFile.writestr("settings.xml", etree.tostring(
|
||
xSett, pretty_print=False, encoding="utf-8", xml_declaration=True
|
||
))
|
||
outFile.writestr("content.xml", etree.tostring(
|
||
self._dCont, pretty_print=False, encoding="utf-8", xml_declaration=True
|
||
))
|
||
outFile.writestr("meta.xml", etree.tostring(
|
||
self._dMeta, pretty_print=False, encoding="utf-8", xml_declaration=True
|
||
))
|
||
outFile.writestr("styles.xml", etree.tostring(
|
||
self._dStyl, pretty_print=False, encoding="utf-8", xml_declaration=True
|
||
))
|
||
|
||
return
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _formatSynopsis(self, tText):
|
||
"""Apply formatting to synopsis lines.
|
||
"""
|
||
sSynop = self._localLookup("Synopsis")
|
||
rTxt = "**{0}:** {1}".format(sSynop, tText)
|
||
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(tText))
|
||
return rTxt, rFmt
|
||
|
||
def _formatComments(self, tText):
|
||
"""Apply formatting to comments.
|
||
"""
|
||
sComm = self._localLookup("Comment")
|
||
rTxt = "**{0}:** {1}".format(sComm, tText)
|
||
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(tText))
|
||
return rTxt, rFmt
|
||
|
||
def _formatKeywords(self, tText):
|
||
"""Apply formatting to keywords.
|
||
"""
|
||
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
|
||
if not isValid or not theBits:
|
||
return ""
|
||
|
||
rTxt = ""
|
||
rFmt = ""
|
||
if theBits[0] in nwLabels.KEY_NAME:
|
||
tText = nwLabels.KEY_NAME[theBits[0]]
|
||
rTxt += "**{0}:** ".format(tText)
|
||
rFmt += "_B{0} b_ ".format(" "*len(tText))
|
||
if len(theBits) > 1:
|
||
if theBits[0] == nwKeyWords.TAG_KEY:
|
||
rTxt += theBits[1]
|
||
rFmt += " "*len(theBits[1])
|
||
else:
|
||
tTags = ", ".join(theBits[1:])
|
||
rTxt += tTags
|
||
rFmt += (" "*len(tTags))
|
||
|
||
return rTxt, rFmt
|
||
|
||
def _addTextPar(self, styleName, oStyle, theText, theFmt="", isHead=False, oLevel=None):
|
||
"""Add a text paragraph to the text XML element.
|
||
"""
|
||
tAttr = {}
|
||
tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle)
|
||
if oLevel is not None:
|
||
tAttr[_mkTag("text", "outline-level")] = oLevel
|
||
|
||
pTag = "h" if isHead else "p"
|
||
xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
|
||
|
||
# It's important to set the initial text field to empty, otherwise
|
||
# lxml will add a line break if the first subelement is a span.
|
||
xElem.text = ""
|
||
|
||
if not theText:
|
||
return
|
||
|
||
##
|
||
# Process Formatting
|
||
##
|
||
|
||
if len(theText) != len(theFmt):
|
||
# Generate an empty format if there isn't any or it doesn't match
|
||
theFmt = " "*len(theText)
|
||
|
||
# The formatting loop
|
||
tTemp = ""
|
||
xFmt = 0x00
|
||
pFmt = 0x00
|
||
pErr = 0
|
||
|
||
parProc = XMLParagraph(xElem)
|
||
|
||
for i, c in enumerate(theText):
|
||
|
||
if theFmt[i] == " ":
|
||
tTemp += c
|
||
elif theFmt[i] == "_":
|
||
continue
|
||
elif theFmt[i] == "B":
|
||
xFmt |= X_BLD
|
||
elif theFmt[i] == "b":
|
||
xFmt &= M_BLD
|
||
elif theFmt[i] == "I":
|
||
xFmt |= X_ITA
|
||
elif theFmt[i] == "i":
|
||
xFmt &= M_ITA
|
||
elif theFmt[i] == "S":
|
||
xFmt |= X_DEL
|
||
elif theFmt[i] == "s":
|
||
xFmt &= M_DEL
|
||
else:
|
||
pErr += 1
|
||
|
||
if xFmt != pFmt:
|
||
if pFmt == 0x00:
|
||
parProc.appendText(tTemp)
|
||
tTemp = ""
|
||
else:
|
||
parProc.appendSpan(tTemp, self._textStyle(pFmt))
|
||
tTemp = ""
|
||
|
||
pFmt = xFmt
|
||
|
||
# Save what remains in the buffer
|
||
if pFmt == 0x00:
|
||
parProc.appendText(tTemp)
|
||
else:
|
||
parProc.appendSpan(tTemp, self._textStyle(pFmt))
|
||
|
||
if pErr > 0:
|
||
self._errData.append("Unknown format tag encountered")
|
||
|
||
nErr, errMsg = parProc.checkError()
|
||
if nErr > 0: # pragma: no cover
|
||
# This one should only capture bugs
|
||
self._errData.append(errMsg)
|
||
|
||
return
|
||
|
||
def _paraStyle(self, parName, oStyle):
|
||
"""Return a name for a style object.
|
||
"""
|
||
refStyle = self._mainPara.get(parName, None)
|
||
if refStyle is None:
|
||
logger.error("Unknown paragraph style '%s'", parName)
|
||
return "Standard"
|
||
|
||
if not refStyle.checkNew(oStyle):
|
||
return parName
|
||
|
||
oStyle.setParentStyleName(parName)
|
||
theID = oStyle.getID()
|
||
if theID in self._autoPara:
|
||
return self._autoPara[theID][0]
|
||
|
||
newName = "P%d" % (len(self._autoPara) + 1)
|
||
self._autoPara[theID] = (newName, oStyle)
|
||
|
||
return newName
|
||
|
||
def _textStyle(self, tFmt):
|
||
"""Return a text style for a given style code.
|
||
"""
|
||
if tFmt in self._autoText:
|
||
return self._autoText[tFmt][0]
|
||
|
||
newName = "T%d" % (len(self._autoText) + 1)
|
||
newStyle = ODTTextStyle()
|
||
if tFmt & X_BLD:
|
||
newStyle.setFontWeight("bold")
|
||
if tFmt & X_ITA:
|
||
newStyle.setFontStyle("italic")
|
||
if tFmt & X_DEL:
|
||
newStyle.setStrikeStyle("solid")
|
||
newStyle.setStrikeType("single")
|
||
|
||
self._autoText[tFmt] = (newName, newStyle)
|
||
|
||
return newName
|
||
|
||
def _emToCm(self, emVal):
|
||
"""Converts an em value to centimetres.
|
||
"""
|
||
return f"{emVal*2.54/72*self.textSize:.3f}cm"
|
||
|
||
##
|
||
# Style Elements
|
||
##
|
||
|
||
def _pageStyles(self):
|
||
"""Set the default page style.
|
||
"""
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = "PM1"
|
||
if self._isFlat:
|
||
xPage = etree.SubElement(self._xAuto, _mkTag("style", "page-layout"), attrib=theAttr)
|
||
else:
|
||
xPage = etree.SubElement(self._xAut2, _mkTag("style", "page-layout"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("fo", "margin-top")] = self._mDocTop
|
||
theAttr[_mkTag("fo", "margin-bottom")] = self._mDocBtm
|
||
theAttr[_mkTag("fo", "margin-left")] = self._mDocLeft
|
||
theAttr[_mkTag("fo", "margin-right")] = self._mDocRight
|
||
etree.SubElement(xPage, _mkTag("style", "page-layout-properties"), attrib=theAttr)
|
||
|
||
xHead = etree.SubElement(xPage, _mkTag("style", "header-style"))
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("fo", "min-height")] = "0.600cm"
|
||
theAttr[_mkTag("fo", "margin-left")] = "0.000cm"
|
||
theAttr[_mkTag("fo", "margin-right")] = "0.000cm"
|
||
theAttr[_mkTag("fo", "margin-bottom")] = "0.500cm"
|
||
etree.SubElement(xHead, _mkTag("style", "header-footer-properties"), attrib=theAttr)
|
||
|
||
return
|
||
|
||
def _defaultStyles(self):
|
||
"""Set the default styles.
|
||
"""
|
||
# Add Paragraph Family Style
|
||
# ==========================
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "family")] = "paragraph"
|
||
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "line-break")] = "strict"
|
||
theAttr[_mkTag("style", "tab-stop-distance")] = "1.251cm"
|
||
theAttr[_mkTag("style", "writing-mode")] = "page"
|
||
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "font-name")] = self.textFont
|
||
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
|
||
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
|
||
theAttr[_mkTag("fo", "language")] = self._dLanguage
|
||
theAttr[_mkTag("fo", "country")] = self._dCountry
|
||
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
|
||
|
||
# Add Standard Paragraph Style
|
||
# ============================
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = "Standard"
|
||
theAttr[_mkTag("style", "family")] = "paragraph"
|
||
theAttr[_mkTag("style", "class")] = "text"
|
||
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "font-name")] = self.textFont
|
||
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
|
||
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
|
||
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
|
||
|
||
# Add Default Heading Style
|
||
# =========================
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = "Heading"
|
||
theAttr[_mkTag("style", "family")] = "paragraph"
|
||
theAttr[_mkTag("style", "parent-style-name")] = "Standard"
|
||
theAttr[_mkTag("style", "next-style-name")] = "Text_Body"
|
||
theAttr[_mkTag("style", "class")] = "text"
|
||
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("fo", "margin-top")] = self._mTopHead
|
||
theAttr[_mkTag("fo", "margin-bottom")] = self._mBotHead
|
||
theAttr[_mkTag("fo", "keep-with-next")] = "always"
|
||
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "font-name")] = self.textFont
|
||
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
|
||
theAttr[_mkTag("fo", "font-size")] = self._fSizeHead
|
||
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
|
||
|
||
# Add Header and Footer Styles
|
||
# ============================
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = "Header_and_Footer"
|
||
theAttr[_mkTag("style", "display-name")] = "Header and Footer"
|
||
theAttr[_mkTag("style", "family")] = "paragraph"
|
||
theAttr[_mkTag("style", "parent-style-name")] = "Standard"
|
||
theAttr[_mkTag("style", "class")] = "extra"
|
||
etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
|
||
|
||
return
|
||
|
||
def _useableStyles(self):
|
||
"""Set the usable styles.
|
||
"""
|
||
# Add Text Body Style
|
||
# ===================
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Text Body")
|
||
oStyle.setParentStyleName("Standard")
|
||
oStyle.setClass("text")
|
||
oStyle.setMarginTop(self._mTopText)
|
||
oStyle.setMarginBottom(self._mBotText)
|
||
oStyle.setLineHeight(self._lineHeight)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeText)
|
||
oStyle.setTextAlign(self._textAlign)
|
||
oStyle.packXML(self._xStyl, "Text_Body")
|
||
|
||
self._mainPara["Text_Body"] = oStyle
|
||
|
||
# Add Text Meta Style
|
||
# ===================
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Text Meta")
|
||
oStyle.setParentStyleName("Standard")
|
||
oStyle.setClass("text")
|
||
oStyle.setMarginTop(self._mTopMeta)
|
||
oStyle.setMarginBottom(self._mBotMeta)
|
||
oStyle.setLineHeight(self._lineHeight)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeText)
|
||
oStyle.setColor(self._colMetaTx)
|
||
oStyle.setOpacity(self._opaMetaTx)
|
||
oStyle.packXML(self._xStyl, "Text_Meta")
|
||
|
||
self._mainPara["Text_Meta"] = oStyle
|
||
|
||
# Add Title Style
|
||
# ===============
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Title")
|
||
oStyle.setParentStyleName("Heading")
|
||
oStyle.setNextStyleName("Text_Body")
|
||
oStyle.setClass("chapter")
|
||
oStyle.setTextAlign("center")
|
||
oStyle.setMarginTop(self._mTopTitle)
|
||
oStyle.setMarginBottom(self._mBotTitle)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeTitle)
|
||
oStyle.setFontWeight("bold")
|
||
oStyle.packXML(self._xStyl, "Title")
|
||
|
||
self._mainPara["Title"] = oStyle
|
||
|
||
# Add Heading 1 Style
|
||
# ===================
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Heading 1")
|
||
oStyle.setParentStyleName("Heading")
|
||
oStyle.setNextStyleName("Text_Body")
|
||
oStyle.setOutlineLevel("1")
|
||
oStyle.setClass("text")
|
||
oStyle.setMarginTop(self._mTopHead1)
|
||
oStyle.setMarginBottom(self._mBotHead1)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeHead1)
|
||
oStyle.setColor(self._colHead12)
|
||
oStyle.setOpacity(self._opaHead12)
|
||
oStyle.setFontWeight("bold")
|
||
oStyle.packXML(self._xStyl, "Heading_1")
|
||
|
||
self._mainPara["Heading_1"] = oStyle
|
||
|
||
# Add Heading 2 Style
|
||
# ===================
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Heading 2")
|
||
oStyle.setParentStyleName("Heading")
|
||
oStyle.setNextStyleName("Text_Body")
|
||
oStyle.setOutlineLevel("2")
|
||
oStyle.setClass("text")
|
||
oStyle.setMarginTop(self._mTopHead2)
|
||
oStyle.setMarginBottom(self._mBotHead2)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeHead2)
|
||
oStyle.setColor(self._colHead12)
|
||
oStyle.setOpacity(self._opaHead12)
|
||
oStyle.setFontWeight("bold")
|
||
oStyle.packXML(self._xStyl, "Heading_2")
|
||
|
||
self._mainPara["Heading_2"] = oStyle
|
||
|
||
# Add Heading 3 Style
|
||
# ===================
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Heading 3")
|
||
oStyle.setParentStyleName("Heading")
|
||
oStyle.setNextStyleName("Text_Body")
|
||
oStyle.setOutlineLevel("3")
|
||
oStyle.setClass("text")
|
||
oStyle.setMarginTop(self._mTopHead3)
|
||
oStyle.setMarginBottom(self._mBotHead3)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeHead3)
|
||
oStyle.setColor(self._colHead34)
|
||
oStyle.setOpacity(self._opaHead34)
|
||
oStyle.setFontWeight("bold")
|
||
oStyle.packXML(self._xStyl, "Heading_3")
|
||
|
||
self._mainPara["Heading_3"] = oStyle
|
||
|
||
# Add Heading 4 Style
|
||
# ===================
|
||
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Heading 4")
|
||
oStyle.setParentStyleName("Heading")
|
||
oStyle.setNextStyleName("Text_Body")
|
||
oStyle.setOutlineLevel("4")
|
||
oStyle.setClass("text")
|
||
oStyle.setMarginTop(self._mTopHead4)
|
||
oStyle.setMarginBottom(self._mBotHead4)
|
||
oStyle.setFontName(self.textFont)
|
||
oStyle.setFontFamily(self._fontFamily)
|
||
oStyle.setFontSize(self._fSizeHead4)
|
||
oStyle.setColor(self._colHead34)
|
||
oStyle.setOpacity(self._opaHead34)
|
||
oStyle.setFontWeight("bold")
|
||
oStyle.packXML(self._xStyl, "Heading_4")
|
||
|
||
self._mainPara["Heading_4"] = oStyle
|
||
|
||
# Add Header Style
|
||
# ================
|
||
oStyle = ODTParagraphStyle()
|
||
oStyle.setDisplayName("Header")
|
||
oStyle.setParentStyleName("Header_and_Footer")
|
||
oStyle.setTextAlign("right")
|
||
oStyle.packXML(self._xStyl, "Header")
|
||
|
||
self._mainPara["Header"] = oStyle
|
||
|
||
return
|
||
|
||
def _writeHeader(self):
|
||
"""Write the header elements.
|
||
"""
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = "Standard"
|
||
theAttr[_mkTag("style", "page-layout-name")] = "PM1"
|
||
xPage = etree.SubElement(self._xMast, _mkTag("style", "master-page"), attrib=theAttr)
|
||
|
||
# Standard Page Header
|
||
xHead = etree.SubElement(xPage, _mkTag("style", "header"))
|
||
xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={
|
||
_mkTag("text", "style-name"): "Header"
|
||
})
|
||
xPar.text = self.headerText.strip() + " "
|
||
|
||
xTail = etree.SubElement(xPar, _mkTag("text", "page-number"), attrib={
|
||
_mkTag("text", "select-page"): "current"
|
||
})
|
||
xTail.text = "2"
|
||
|
||
# First Page Header
|
||
xHead = etree.SubElement(xPage, _mkTag("style", "header-first"))
|
||
xPar = etree.SubElement(xHead, _mkTag("text", "p"), attrib={
|
||
_mkTag("text", "style-name"): "Header"
|
||
})
|
||
|
||
return
|
||
|
||
# END Class ToOdt
|
||
|
||
|
||
# =============================================================================================== #
|
||
# Auto-Style Classes
|
||
# =============================================================================================== #
|
||
|
||
class ODTParagraphStyle():
|
||
"""Wrapper class for the paragraph style setting used by the
|
||
exporter. Only the used settings are exposed here to keep the class
|
||
minimal and fast.
|
||
"""
|
||
VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
|
||
VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
|
||
VALID_LEVEL = ["1", "2", "3", "4"]
|
||
VALID_CLASS = ["text", "chapter"]
|
||
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
||
|
||
def __init__(self):
|
||
|
||
# Attributes
|
||
self._mAttr = {
|
||
"display-name": ["style", None],
|
||
"parent-style-name": ["style", None],
|
||
"next-style-name": ["style", None],
|
||
"default-outline-level": ["style", None],
|
||
"class": ["style", None],
|
||
}
|
||
|
||
# Paragraph Attributes
|
||
self._pAttr = {
|
||
"margin-top": ["fo", None],
|
||
"margin-bottom": ["fo", None],
|
||
"margin-left": ["fo", None],
|
||
"margin-right": ["fo", None],
|
||
"line-height": ["fo", None],
|
||
"text-align": ["fo", None],
|
||
"break-before": ["fo", None],
|
||
"break-after": ["fo", None],
|
||
}
|
||
|
||
# Text Attributes
|
||
self._tAttr = {
|
||
"font-name": ["style", None],
|
||
"font-family": ["fo", None],
|
||
"font-size": ["fo", None],
|
||
"font-weight": ["fo", None],
|
||
"color": ["fo", None],
|
||
"opacity": ["loext", None],
|
||
}
|
||
|
||
return
|
||
|
||
##
|
||
# Attribute Setters
|
||
##
|
||
|
||
def setDisplayName(self, theValue):
|
||
self._mAttr["display-name"][1] = str(theValue)
|
||
return
|
||
|
||
def setParentStyleName(self, theValue):
|
||
self._mAttr["parent-style-name"][1] = str(theValue)
|
||
return
|
||
|
||
def setNextStyleName(self, theValue):
|
||
self._mAttr["next-style-name"][1] = str(theValue)
|
||
return
|
||
|
||
def setOutlineLevel(self, theValue):
|
||
if theValue in self.VALID_LEVEL:
|
||
self._mAttr["default-outline-level"][1] = str(theValue)
|
||
else:
|
||
self._mAttr["default-outline-level"][1] = None
|
||
return
|
||
|
||
def setClass(self, theValue):
|
||
if theValue in self.VALID_CLASS:
|
||
self._mAttr["class"][1] = str(theValue)
|
||
else:
|
||
self._mAttr["class"][1] = None
|
||
return
|
||
|
||
##
|
||
# Paragraph Setters
|
||
##
|
||
|
||
def setMarginTop(self, theValue):
|
||
self._pAttr["margin-top"][1] = str(theValue)
|
||
return
|
||
|
||
def setMarginBottom(self, theValue):
|
||
self._pAttr["margin-bottom"][1] = str(theValue)
|
||
return
|
||
|
||
def setMarginLeft(self, theValue):
|
||
self._pAttr["margin-left"][1] = str(theValue)
|
||
return
|
||
|
||
def setMarginRight(self, theValue):
|
||
self._pAttr["margin-right"][1] = str(theValue)
|
||
return
|
||
|
||
def setLineHeight(self, theValue):
|
||
self._pAttr["line-height"][1] = str(theValue)
|
||
return
|
||
|
||
def setTextAlign(self, theValue):
|
||
if theValue in self.VALID_ALIGN:
|
||
self._pAttr["text-align"][1] = str(theValue)
|
||
else:
|
||
self._pAttr["text-align"][1] = None
|
||
return
|
||
|
||
def setBreakBefore(self, theValue):
|
||
if theValue in self.VALID_BREAK:
|
||
self._pAttr["break-before"][1] = str(theValue)
|
||
else:
|
||
self._pAttr["break-before"][1] = None
|
||
return
|
||
|
||
def setBreakAfter(self, theValue):
|
||
if theValue in self.VALID_BREAK:
|
||
self._pAttr["break-after"][1] = str(theValue)
|
||
else:
|
||
self._pAttr["break-after"][1] = None
|
||
return
|
||
|
||
##
|
||
# Text Setters
|
||
##
|
||
|
||
def setFontName(self, theValue):
|
||
self._tAttr["font-name"][1] = str(theValue)
|
||
return
|
||
|
||
def setFontFamily(self, theValue):
|
||
self._tAttr["font-family"][1] = str(theValue)
|
||
return
|
||
|
||
def setFontSize(self, theValue):
|
||
self._tAttr["font-size"][1] = str(theValue)
|
||
return
|
||
|
||
def setFontWeight(self, theValue):
|
||
if theValue in self.VALID_WEIGHT:
|
||
self._tAttr["font-weight"][1] = str(theValue)
|
||
else:
|
||
self._tAttr["font-weight"][1] = None
|
||
return
|
||
|
||
def setColor(self, theValue):
|
||
self._tAttr["color"][1] = str(theValue)
|
||
return
|
||
|
||
def setOpacity(self, theValue):
|
||
self._tAttr["opacity"][1] = str(theValue)
|
||
return
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def checkNew(self, refStyle):
|
||
"""Check if there are new settings in refStyle that differ from
|
||
those in the current object.
|
||
"""
|
||
for aName, (_, aVal) in refStyle._mAttr.items():
|
||
if aVal is not None and aVal != self._mAttr[aName][1]:
|
||
return True
|
||
for aName, (_, aVal) in refStyle._pAttr.items():
|
||
if aVal is not None and aVal != self._pAttr[aName][1]:
|
||
return True
|
||
for aName, (_, aVal) in refStyle._tAttr.items():
|
||
if aVal is not None and aVal != self._tAttr[aName][1]:
|
||
return True
|
||
return False
|
||
|
||
def getID(self):
|
||
"""Generate a unique ID from the settings.
|
||
"""
|
||
theString = (
|
||
f"Paragraph:Main:{str(self._mAttr)}:"
|
||
f"Paragraph:Para:{str(self._pAttr)}:"
|
||
f"Paragraph:Text:{str(self._tAttr)}:"
|
||
)
|
||
return sha256(theString.encode()).hexdigest()
|
||
|
||
def packXML(self, xParent, xName):
|
||
"""Pack the content into an xml element.
|
||
"""
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = xName
|
||
theAttr[_mkTag("style", "family")] = "paragraph"
|
||
for aName, (aNm, aVal) in self._mAttr.items():
|
||
if aVal is not None:
|
||
theAttr[_mkTag(aNm, aName)] = aVal
|
||
|
||
xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
for aName, (aNm, aVal) in self._pAttr.items():
|
||
if aVal is not None:
|
||
theAttr[_mkTag(aNm, aName)] = aVal
|
||
|
||
if theAttr:
|
||
etree.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
for aName, (aNm, aVal) in self._tAttr.items():
|
||
if aVal is not None:
|
||
theAttr[_mkTag(aNm, aName)] = aVal
|
||
|
||
if theAttr:
|
||
etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
|
||
|
||
return
|
||
|
||
# END Class ODTParagraphStyle
|
||
|
||
|
||
class ODTTextStyle():
|
||
"""Wrapper class for the text style setting used by the exporter.
|
||
Only the used settings are exposed here to keep the class minimal
|
||
and fast.
|
||
"""
|
||
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
||
VALID_STYLE = ["normal", "inherit", "italic"]
|
||
VALID_LSTYLE = ["none", "solid"]
|
||
VALID_LTYPE = ["none", "single", "double"]
|
||
|
||
def __init__(self):
|
||
|
||
# Text Attributes
|
||
self._tAttr = {
|
||
"font-weight": ["fo", None],
|
||
"font-style": ["fo", None],
|
||
"text-line-through-style": ["style", None],
|
||
"text-line-through-type": ["style", None],
|
||
}
|
||
|
||
return
|
||
|
||
##
|
||
# Setters
|
||
##
|
||
|
||
def setFontWeight(self, theValue):
|
||
if theValue in self.VALID_WEIGHT:
|
||
self._tAttr["font-weight"][1] = str(theValue)
|
||
else:
|
||
self._tAttr["font-weight"][1] = None
|
||
return
|
||
|
||
def setFontStyle(self, theValue):
|
||
if theValue in self.VALID_STYLE:
|
||
self._tAttr["font-style"][1] = str(theValue)
|
||
else:
|
||
self._tAttr["font-style"][1] = None
|
||
return
|
||
|
||
def setStrikeStyle(self, theValue):
|
||
if theValue in self.VALID_LSTYLE:
|
||
self._tAttr["text-line-through-style"][1] = str(theValue)
|
||
else:
|
||
self._tAttr["text-line-through-style"][1] = None
|
||
return
|
||
|
||
def setStrikeType(self, theValue):
|
||
if theValue in self.VALID_LTYPE:
|
||
self._tAttr["text-line-through-type"][1] = str(theValue)
|
||
else:
|
||
self._tAttr["text-line-through-type"][1] = None
|
||
return
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def packXML(self, xParent, xName):
|
||
"""Pack the content into an xml element.
|
||
"""
|
||
theAttr = {}
|
||
theAttr[_mkTag("style", "name")] = xName
|
||
theAttr[_mkTag("style", "family")] = "text"
|
||
xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
|
||
|
||
theAttr = {}
|
||
for aName, (aNm, aVal) in self._tAttr.items():
|
||
if aVal is not None:
|
||
theAttr[_mkTag(aNm, aName)] = aVal
|
||
|
||
if theAttr:
|
||
etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
|
||
|
||
return
|
||
|
||
# END Class ODTTextStyle
|
||
|
||
|
||
# =============================================================================================== #
|
||
# XML Complex Element Helper Class
|
||
# =============================================================================================== #
|
||
|
||
X_ROOT_TEXT = 0
|
||
X_ROOT_TAIL = 1
|
||
X_SPAN_TEXT = 2
|
||
X_SPAN_SING = 3
|
||
|
||
|
||
class XMLParagraph():
|
||
"""This is a helper class to manage the text content of a single
|
||
XML element using mixed content tags.
|
||
|
||
See: https://lxml.de/tutorial.html#the-element-class
|
||
|
||
Rules:
|
||
* The root tag can only have text set, never tail.
|
||
* Any span must be under root, and the text in the span is set in
|
||
one pass. The span then becomes the new base element using tail
|
||
for further added text, permanently replacing root.
|
||
* Any single special tags like tabs, line breaks or multi-spaces,
|
||
should never have text set. After insertion, they become the next
|
||
tail tag if on root level, or if in a span, only exists within
|
||
the lifetime of the span. In this case, the span becomes the new
|
||
tail.
|
||
|
||
The four constants associated with this class represent the only
|
||
allowed states the class can exist in, which dictates which XML
|
||
object and attribute is written to,
|
||
"""
|
||
|
||
def __init__(self, xRoot):
|
||
|
||
self._xRoot = xRoot
|
||
self._xTail = None
|
||
self._xSing = None
|
||
|
||
self._nState = X_ROOT_TEXT
|
||
self._chrPos = 0
|
||
self._rawTxt = ""
|
||
self._xRoot.text = ""
|
||
|
||
return
|
||
|
||
def appendText(self, tText):
|
||
"""Append text to the XML element. We do this one character at
|
||
the time in order to be able to process line breaks, tabs and
|
||
spaces separately. Multiple spaces above one are concatenated
|
||
into a single tag, and must therefore be processed separately.
|
||
"""
|
||
nSpaces = 0
|
||
self._rawTxt += tText
|
||
|
||
for c in tText:
|
||
if c == " ":
|
||
nSpaces += 1
|
||
continue
|
||
|
||
elif nSpaces > 0:
|
||
self._processSpaces(nSpaces)
|
||
nSpaces = 0
|
||
|
||
if c == "\n":
|
||
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||
self._xTail = etree.SubElement(self._xRoot, TAG_BR)
|
||
self._xTail.tail = ""
|
||
self._nState = X_ROOT_TAIL
|
||
self._chrPos += 1
|
||
|
||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
||
self._xSing = etree.SubElement(self._xTail, TAG_BR)
|
||
self._xSing.tail = ""
|
||
self._nState = X_SPAN_SING
|
||
self._chrPos += 1
|
||
|
||
elif c == "\t":
|
||
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||
self._xTail = etree.SubElement(self._xRoot, TAG_TAB)
|
||
self._xTail.tail = ""
|
||
self._nState = X_ROOT_TAIL
|
||
self._chrPos += 1
|
||
|
||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
||
self._xSing = etree.SubElement(self._xTail, TAG_TAB)
|
||
self._xSing.tail = ""
|
||
self._chrPos += 1
|
||
self._nState = X_SPAN_SING
|
||
|
||
else:
|
||
if self._nState == X_ROOT_TEXT:
|
||
self._xRoot.text += c
|
||
self._chrPos += 1
|
||
elif self._nState == X_ROOT_TAIL:
|
||
self._xTail.tail += c
|
||
self._chrPos += 1
|
||
elif self._nState == X_SPAN_TEXT:
|
||
self._xTail.text += c
|
||
self._chrPos += 1
|
||
elif self._nState == X_SPAN_SING:
|
||
self._xSing.tail += c
|
||
self._chrPos += 1
|
||
|
||
if nSpaces > 0:
|
||
self._processSpaces(nSpaces)
|
||
|
||
return
|
||
|
||
def appendSpan(self, tText, tFmt):
|
||
"""Append a text span to the XML element. The span is always
|
||
closed since we do not allow nested spans (like Libre Office).
|
||
Therefore we return to the root element level when we're done
|
||
processing the text of the span.
|
||
"""
|
||
self._xTail = etree.SubElement(self._xRoot, TAG_SPAN, attrib={
|
||
TAG_STNM: tFmt
|
||
})
|
||
self._xTail.text = "" # Defaults to None
|
||
self._xTail.tail = "" # Defaults to None
|
||
self._nState = X_SPAN_TEXT
|
||
self.appendText(tText)
|
||
self._nState = X_ROOT_TAIL
|
||
|
||
return
|
||
|
||
def checkError(self):
|
||
"""Check that the number of characters written matches the
|
||
number of characters received."""
|
||
errMsg = ""
|
||
nMissed = len(self._rawTxt) - self._chrPos
|
||
if nMissed != 0:
|
||
errMsg = "%d char(s) were not written: '%s'" % (nMissed, self._rawTxt)
|
||
return nMissed, errMsg
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _processSpaces(self, nSpaces):
|
||
"""Add spaces to paragraph. The first space is always written
|
||
as-is (unless it's the first character of the paragraph). The
|
||
second space uses the dedicated tag for spaces, and from the
|
||
third space and on, a counter is added to the tag.
|
||
|
||
See: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html
|
||
Sections: 6.1.2, 6.1.3, and 19.763
|
||
"""
|
||
if nSpaces > 0:
|
||
if self._chrPos > 0:
|
||
if self._nState == X_ROOT_TEXT:
|
||
self._xRoot.text += " "
|
||
self._chrPos += 1
|
||
elif self._nState == X_ROOT_TAIL:
|
||
self._xTail.tail += " "
|
||
self._chrPos += 1
|
||
elif self._nState == X_SPAN_TEXT:
|
||
self._xTail.text += " "
|
||
self._chrPos += 1
|
||
elif self._nState == X_SPAN_SING:
|
||
self._xSing.tail += " "
|
||
self._chrPos += 1
|
||
else:
|
||
nSpaces += 1
|
||
|
||
if nSpaces == 2:
|
||
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||
self._xTail = etree.SubElement(self._xRoot, TAG_SPC)
|
||
self._xTail.tail = ""
|
||
self._nState = X_ROOT_TAIL
|
||
self._chrPos += nSpaces - 1
|
||
|
||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
||
self._xSing = etree.SubElement(self._xTail, TAG_SPC)
|
||
self._xSing.tail = ""
|
||
self._nState = X_SPAN_SING
|
||
self._chrPos += nSpaces - 1
|
||
|
||
elif nSpaces > 2:
|
||
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||
self._xTail = etree.SubElement(self._xRoot, TAG_SPC, attrib={
|
||
TAG_NSPC: str(nSpaces - 1)
|
||
})
|
||
self._xTail.tail = ""
|
||
self._nState = X_ROOT_TAIL
|
||
self._chrPos += nSpaces - 1
|
||
|
||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
||
self._xSing = etree.SubElement(self._xTail, TAG_SPC, attrib={
|
||
TAG_NSPC: str(nSpaces - 1)
|
||
})
|
||
self._xSing.tail = ""
|
||
self._nState = X_SPAN_SING
|
||
self._chrPos += nSpaces - 1
|
||
|
||
return
|
||
|
||
# END Class XMLParagraph
|