Add DocX support (#2056)

This commit is contained in:
Veronica Berglyd Olsen
2024-10-21 17:13:42 +02:00
committed by GitHub
29 changed files with 3369 additions and 150 deletions
+26
View File
@@ -204,6 +204,14 @@ def checkIntTuple(value: int, valid: tuple | list | set, default: int) -> int:
return default
def firstFloat(*args: Any) -> float:
"""Return the first value that is a float."""
for arg in args:
if isinstance(arg, float):
return arg
return 0.0
##
# Formatting Functions
##
@@ -515,6 +523,24 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
return
def xmlSubElem(
parent: ET.Element,
tag: str,
text: str | int | float | bool | None = None,
attrib: dict | None = None
) -> ET.Element:
"""A custom implementation of SubElement that takes text as an
argument.
"""
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
if text is not None:
if isinstance(text, bool):
xSub.text = str(text).lower()
else:
xSub.text = str(text)
return xSub
##
# File and File System Functions
##
+7 -5
View File
@@ -267,6 +267,7 @@ class nwLabels:
BUILD_FMT = {
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)"),
nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
@@ -278,6 +279,7 @@ class nwLabels:
BUILD_EXT = {
nwBuildFmt.ODT: ".odt",
nwBuildFmt.FODT: ".fodt",
nwBuildFmt.DOCX: ".docx",
nwBuildFmt.HTML: ".html",
nwBuildFmt.NWD: ".txt",
nwBuildFmt.STD_MD: ".md",
@@ -367,11 +369,11 @@ class nwHeadFmt:
CHAR_POV, CHAR_FOCUS
]
# ODT Document Page Header
ODT_PROJECT = "{Project}"
ODT_AUTHOR = "{Author}"
ODT_PAGE = "{Page}"
ODT_AUTO = "{Project} / {Author} / {Page}"
# Document Page Header
DOC_PROJECT = "{Project}"
DOC_AUTHOR = "{Author}"
DOC_PAGE = "{Page}"
DOC_AUTO = "{Project} / {Author} / {Page}"
class nwQuotes:
+18 -12
View File
@@ -109,11 +109,11 @@ SETTINGS_TEMPLATE: dict[str, tuple[type, str | int | float | bool]] = {
"format.bottomMargin": (float, 2.0),
"format.leftMargin": (float, 2.0),
"format.rightMargin": (float, 2.0),
"odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
"odt.pageCountOffset": (int, 0),
"odt.colorHeadings": (bool, True),
"odt.scaleHeadings": (bool, True),
"odt.boldHeadings": (bool, True),
"doc.pageHeader": (str, nwHeadFmt.DOC_AUTO),
"doc.pageCountOffset": (int, 0),
"doc.colorHeadings": (bool, True),
"doc.scaleHeadings": (bool, True),
"doc.boldHeadings": (bool, True),
"html.addStyles": (bool, True),
"html.preserveTabs": (bool, False),
}
@@ -165,18 +165,24 @@ SETTINGS_LABELS = {
"format.pageSize": QT_TRANSLATE_NOOP("Builds", "Page Size"),
"format.pageMargins": QT_TRANSLATE_NOOP("Builds", "Page Margins"),
"odt": QT_TRANSLATE_NOOP("Builds", "Document Options"),
"odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
"odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
"odt.colorHeadings": QT_TRANSLATE_NOOP("Builds", "Add Colours to Headings"),
"odt.scaleHeadings": QT_TRANSLATE_NOOP("Builds", "Increase Size of Headings"),
"odt.boldHeadings": QT_TRANSLATE_NOOP("Builds", "Bold Headings"),
"doc": QT_TRANSLATE_NOOP("Builds", "Document Style"),
"doc.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
"doc.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
"doc.colorHeadings": QT_TRANSLATE_NOOP("Builds", "Add Colours to Headings"),
"doc.scaleHeadings": QT_TRANSLATE_NOOP("Builds", "Increase Size of Headings"),
"doc.boldHeadings": QT_TRANSLATE_NOOP("Builds", "Bold Headings"),
"html": QT_TRANSLATE_NOOP("Builds", "HTML Options"),
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
"html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
}
RENAMED = {
"odt.addColours": "doc.addColours",
"odt.pageHeader": "doc.pageHeader",
"odt.pageCountOffset": "doc.pageCountOffset",
}
class FilterMode(Enum):
"""The decision reason for an item in a filtered project."""
@@ -490,7 +496,7 @@ class BuildSettings:
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
if isinstance(settings, dict):
for key, value in settings.items():
self.setValue(key, value)
self.setValue(RENAMED.get(key, key), value)
self._changed = False
+17 -7
View File
@@ -37,6 +37,7 @@ from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException
from novelwriter.formats.todocx import ToDocX
from novelwriter.formats.tohtml import ToHtml
from novelwriter.formats.tokenizer import Tokenizer
from novelwriter.formats.tomarkdown import ToMarkdown
@@ -185,6 +186,15 @@ class NWBuildDocument:
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
elif bFormat == nwBuildFmt.DOCX:
makeObj = ToDocX(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
elif bFormat == nwBuildFmt.PDF:
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
@@ -282,9 +292,9 @@ class NWBuildDocument:
self._build.getBool("format.indentFirstPar"),
)
bldObj.setHeadingStyles(
self._build.getBool("odt.colorHeadings"),
self._build.getBool("odt.scaleHeadings"),
self._build.getBool("odt.boldHeadings"),
self._build.getBool("doc.colorHeadings"),
self._build.getBool("doc.scaleHeadings"),
self._build.getBool("doc.boldHeadings"),
)
bldObj.setTitleMargins(
@@ -326,14 +336,14 @@ class NWBuildDocument:
bldObj.setStyles(self._build.getBool("html.addStyles"))
bldObj.setReplaceUnicode(self._build.getBool("format.stripUnicode"))
if isinstance(bldObj, ToOdt):
if isinstance(bldObj, (ToOdt, ToDocX)):
bldObj.setLanguage(self._project.data.language)
bldObj.setHeaderFormat(
self._build.getStr("odt.pageHeader"),
self._build.getInt("odt.pageCountOffset"),
self._build.getStr("doc.pageHeader"),
self._build.getInt("doc.pageCountOffset"),
)
if isinstance(bldObj, (ToOdt, ToQTextDocument)):
if isinstance(bldObj, (ToOdt, ToDocX, ToQTextDocument)):
scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0)
pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0))
bldObj.setPageLayout(
+8 -7
View File
@@ -180,13 +180,14 @@ class nwBuildFmt(Enum):
ODT = 0
FODT = 1
HTML = 2
NWD = 3
STD_MD = 4
EXT_MD = 5
PDF = 6
J_HTML = 7
J_NWD = 8
DOCX = 2
HTML = 3
NWD = 4
STD_MD = 5
EXT_MD = 6
PDF = 7
J_HTML = 8
J_NWD = 9
class nwStatusShape(Enum):
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -372,7 +372,7 @@ class ToHtml(Tokenizer):
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
"}}"
).format(
"justify" if self._doJustify else "left",
"justify" if self._doJustify else self._defaultAlign,
round(100 * self._lineHeight),
mScale * self._marginText[0],
mScale * self._marginText[1],
+16 -17
View File
@@ -152,21 +152,22 @@ class Tokenizer(ABC):
# User Settings
self._textFont = QFont("Serif", 11) # Output text font
self._lineHeight = 1.15 # Line height in units of em
self._colorHeads = True # Colourise headings
self._scaleHeads = True # Scale headings to larger font size
self._boldHeads = True # Bold headings
self._blockIndent = 4.00 # Block indent in units of em
self._firstIndent = False # Enable first line indent
self._firstWidth = 1.40 # First line indent in units of em
self._indentFirst = False # Indent first paragraph
self._doJustify = False # Justify text
self._doBodyText = True # Include body text
self._doSynopsis = False # Also process synopsis comments
self._doComments = False # Also process comments
self._doKeywords = False # Also process keywords like tags and references
self._skipKeywords = set() # Keywords to ignore
self._keepBreaks = True # Keep line breaks in paragraphs
self._lineHeight = 1.15 # Line height in units of em
self._colorHeads = True # Colourise headings
self._scaleHeads = True # Scale headings to larger font size
self._boldHeads = True # Bold headings
self._blockIndent = 4.00 # Block indent in units of em
self._firstIndent = False # Enable first line indent
self._firstWidth = 1.40 # First line indent in units of em
self._indentFirst = False # Indent first paragraph
self._doJustify = False # Justify text
self._doBodyText = True # Include body text
self._doSynopsis = False # Also process synopsis comments
self._doComments = False # Also process comments
self._doKeywords = False # Also process keywords like tags and references
self._skipKeywords = set() # Keywords to ignore
self._keepBreaks = True # Keep line breaks in paragraphs
self._defaultAlign = "left" # The default text alignment
# Margins
self._marginTitle = nwStyles.T_MARGIN["H0"]
@@ -205,7 +206,6 @@ class Tokenizer(ABC):
self._hFormatter = HeadingFormatter(self._project)
self._noSep = True # Flag to indicate that we don't want a scene separator
self._noIndent = False # Flag to disable text indent on next paragraph
self._showDialog = False # Flag for dialogue highlighting
# This File
self._isNovel = False # Document is a novel document
@@ -380,7 +380,6 @@ class Tokenizer(ABC):
def setDialogueHighlight(self, state: bool) -> None:
"""Enable or disable dialogue highlighting."""
self._rxDialogue = []
self._showDialog = state
if state:
if CONFIG.dialogStyle > 0:
self._rxDialogue.append((
+45 -49
View File
@@ -33,12 +33,12 @@ from collections.abc import Sequence
from datetime import datetime
from hashlib import sha256
from pathlib import Path
from zipfile import ZipFile
from zipfile import ZIP_DEFLATED, ZipFile
from PyQt5.QtGui import QFont
from novelwriter import __version__
from novelwriter.common import xmlIndent
from novelwriter.common import xmlIndent, xmlSubElem
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.formats.tokenizer import T_Formats, Tokenizer, stripEscape
@@ -224,8 +224,8 @@ class ToOdt(Tokenizer):
self._opaHead12 = None
self._colHead34 = None
self._opaHead34 = None
self._colDialogM = None
self._colDialogA = None
self._colDialogM = "#2a6099"
self._colDialogA = "#813709"
self._colMetaTx = "#813709"
self._opaMetaTx = "100%"
self._markText = "#ffffa6"
@@ -245,8 +245,7 @@ class ToOdt(Tokenizer):
return
def setPageLayout(
self, width: int | float, height: int | float,
top: int | float, bottom: int | float, left: int | float, right: int | float
self, width: float, height: float, top: float, bottom: float, left: float, right: float
) -> None:
"""Set the document page size and margins in millimetres."""
self._mDocWidth = f"{width/10.0:.3f}cm"
@@ -325,14 +324,10 @@ class ToOdt(Tokenizer):
self._colHead34 = "#444444"
self._opaHead34 = "100%"
if self._showDialog:
self._colDialogM = "#2a6099"
self._colDialogA = "#813709"
self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
self._fBlockIndent = self._emToCm(self._blockIndent)
self._fTextIndent = self._emToCm(self._firstWidth)
self._textAlign = "justify" if self._doJustify else "left"
self._textAlign = "justify" if self._doJustify else self._defaultAlign
# Clear Errors
self._errData = []
@@ -403,33 +398,21 @@ class ToOdt(Tokenizer):
timeStamp = datetime.now().isoformat(sep="T", timespec="seconds")
# Office Meta Data
xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = timeStamp
xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{__version__}"
xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "initial-creator"))
xMeta.text = self._project.data.author
xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "editing-cycles"))
xMeta.text = str(self._project.data.saveCount)
xmlSubElem(self._xMeta, _mkTag("meta", "creation-date"), timeStamp)
xmlSubElem(self._xMeta, _mkTag("meta", "generator"), f"novelWriter/{__version__}")
xmlSubElem(self._xMeta, _mkTag("meta", "initial-creator"), self._project.data.author)
xmlSubElem(self._xMeta, _mkTag("meta", "editing-cycles"), self._project.data.saveCount)
# Format is: PnYnMnDTnHnMnS
# https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#duration
eT = self._project.data.editTime
xMeta = ET.SubElement(self._xMeta, _mkTag("meta", "editing-duration"))
xMeta.text = f"P{eT//86400:d}DT{eT%86400//3600:d}H{eT%3600//60:d}M{eT%60:d}S"
fT = f"P{eT//86400:d}DT{eT%86400//3600:d}H{eT%3600//60:d}M{eT%60:d}S"
xmlSubElem(self._xMeta, _mkTag("meta", "editing-duration"), fT)
# Dublin Core Meta Data
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title"))
xMeta.text = self._project.data.name
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date"))
xMeta.text = timeStamp
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "creator"))
xMeta.text = self._project.data.author
xmlSubElem(self._xMeta, _mkTag("dc", "title"), self._project.data.name)
xmlSubElem(self._xMeta, _mkTag("dc", "date"), timeStamp)
xmlSubElem(self._xMeta, _mkTag("dc", "creator"), self._project.data.author)
self._pageStyles()
self._defaultStyles()
@@ -474,6 +457,9 @@ class ToOdt(Tokenizer):
# Process Text Types
if tType == self.T_TEXT:
if self._doJustify and "\n" in tText:
oStyle.overrideJustify(self._defaultAlign)
# Text indentation is processed here because there is a
# dedicated pre-defined style for it
if tStyle & self.A_IND_T:
@@ -569,18 +555,18 @@ class ToOdt(Tokenizer):
oVers = _mkTag("office", "version")
xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
def putInZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
with zipObj.open(name, mode="w") as fObj:
xml = ET.ElementTree(xObj)
xml.write(fObj, encoding="utf-8", xml_declaration=True)
with ZipFile(path, mode="w") as outZip:
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
outZip.writestr("mimetype", X_MIME)
putInZip("META-INF/manifest.xml", xMani, outZip)
putInZip("settings.xml", xSett, outZip)
putInZip("content.xml", self._dCont, outZip)
putInZip("meta.xml", self._dMeta, outZip)
putInZip("styles.xml", self._dStyl, outZip)
xmlToZip("META-INF/manifest.xml", xMani, outZip)
xmlToZip("settings.xml", xSett, outZip)
xmlToZip("content.xml", self._dCont, outZip)
xmlToZip("meta.xml", self._dMeta, outZip)
xmlToZip("styles.xml", self._dStyl, outZip)
logger.info("Wrote file: %s", path)
@@ -625,8 +611,13 @@ class ToOdt(Tokenizer):
return rTxt, rFmt
def _addTextPar(
self, xParent: ET.Element, styleName: str, oStyle: ODTParagraphStyle, tText: str,
tFmt: Sequence[tuple[int, int, str]] = [], isHead: bool = False, oLevel: str | None = None
self,
xParent: ET.Element,
styleName: str, oStyle: ODTParagraphStyle,
tText: str,
tFmt: Sequence[tuple[int, int, str]] | None = None,
isHead: bool = False,
oLevel: str | None = None,
) -> None:
"""Add a text paragraph to the text XML element."""
tAttr = {_mkTag("text", "style-name"): self._paraStyle(styleName, oStyle)}
@@ -653,7 +644,7 @@ class ToOdt(Tokenizer):
tFrag = ""
fLast = 0
xNode = None
for fPos, fFmt, fData in tFmt:
for fPos, fFmt, fData in tFmt or []:
# Add any extra nodes
if xNode is not None:
@@ -794,8 +785,7 @@ class ToOdt(Tokenizer):
_mkTag("text", "id"): f"ftn{self._nNote}",
_mkTag("text", "note-class"): "footnote",
})
xCite = ET.SubElement(xNote, _mkTag("text", "note-citation"))
xCite.text = str(self._nNote)
xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote)
xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
return xNote
@@ -1086,12 +1076,12 @@ class ToOdt(Tokenizer):
# Standard Page Header
if self._headerFormat:
pre, page, post = self._headerFormat.partition(nwHeadFmt.ODT_PAGE)
pre, page, post = self._headerFormat.partition(nwHeadFmt.DOC_PAGE)
pre = pre.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name)
pre = pre.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author)
post = post.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name)
post = post.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author)
pre = pre.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
pre = pre.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
post = post.replace(nwHeadFmt.DOC_PROJECT, self._project.data.name)
post = post.replace(nwHeadFmt.DOC_AUTHOR, self._project.data.author)
xHead = ET.SubElement(xPage, _mkTag("style", "header"))
xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
@@ -1326,6 +1316,12 @@ class ODTParagraphStyle:
# Methods
##
def overrideJustify(self, default: str) -> None:
"""Override inherited justify setting if None is set."""
if self._pAttr["text-align"][1] is None:
self.setTextAlign(default)
return
def checkNew(self, style: ODTParagraphStyle) -> bool:
"""Check if there are new settings in style that differ from
those in this object. Unset styles are ignored as they can be
+20 -20
View File
@@ -1226,7 +1226,7 @@ class _FormattingTab(NScrollableForm):
# Open Document
# =============
title = self._build.getLabel("odt")
title = self._build.getLabel("doc")
section += 1
self._sidebar.addButton(title, section)
self.addGroupLabel(title, section)
@@ -1237,7 +1237,7 @@ class _FormattingTab(NScrollableForm):
self.btnPageHeader = NIconToolButton(self, iSz, "revert")
self.btnPageHeader.clicked.connect(self._resetPageHeader)
self.addRow(
self._build.getLabel("odt.pageHeader"), self.odtPageHeader,
self._build.getLabel("doc.pageHeader"), self.odtPageHeader,
button=self.btnPageHeader, stretch=(1, 1)
)
@@ -1246,16 +1246,16 @@ class _FormattingTab(NScrollableForm):
self.odtPageCountOffset.setMaximum(999)
self.odtPageCountOffset.setSingleStep(1)
self.odtPageCountOffset.setMinimumWidth(spW)
self.addRow(self._build.getLabel("odt.pageCountOffset"), self.odtPageCountOffset)
self.addRow(self._build.getLabel("doc.pageCountOffset"), self.odtPageCountOffset)
# Headings
self.colorHeadings = NSwitch(self, height=iPx)
self.scaleHeadings = NSwitch(self, height=iPx)
self.boldHeadings = NSwitch(self, height=iPx)
self.addRow(self._build.getLabel("odt.colorHeadings"), self.colorHeadings)
self.addRow(self._build.getLabel("odt.scaleHeadings"), self.scaleHeadings)
self.addRow(self._build.getLabel("odt.boldHeadings"), self.boldHeadings)
self.addRow(self._build.getLabel("doc.colorHeadings"), self.colorHeadings)
self.addRow(self._build.getLabel("doc.scaleHeadings"), self.scaleHeadings)
self.addRow(self._build.getLabel("doc.boldHeadings"), self.boldHeadings)
# HTML Document
# =============
@@ -1356,14 +1356,14 @@ class _FormattingTab(NScrollableForm):
self.pageUnit.currentIndexChanged.connect(self._changeUnit)
self.pageSize.currentIndexChanged.connect(self._changePageSize)
# ODT Document
# ============
# Document
# ========
self.colorHeadings.setChecked(self._build.getBool("odt.colorHeadings"))
self.scaleHeadings.setChecked(self._build.getBool("odt.scaleHeadings"))
self.boldHeadings.setChecked(self._build.getBool("odt.boldHeadings"))
self.odtPageHeader.setText(self._build.getStr("odt.pageHeader"))
self.odtPageCountOffset.setValue(self._build.getInt("odt.pageCountOffset"))
self.colorHeadings.setChecked(self._build.getBool("doc.colorHeadings"))
self.scaleHeadings.setChecked(self._build.getBool("doc.scaleHeadings"))
self.boldHeadings.setChecked(self._build.getBool("doc.boldHeadings"))
self.odtPageHeader.setText(self._build.getStr("doc.pageHeader"))
self.odtPageCountOffset.setValue(self._build.getInt("doc.pageCountOffset"))
self.odtPageHeader.setCursorPosition(0)
# HTML Document
@@ -1426,12 +1426,12 @@ class _FormattingTab(NScrollableForm):
self._build.setValue("format.leftMargin", self.leftMargin.value())
self._build.setValue("format.rightMargin", self.rightMargin.value())
# ODT Document
self._build.setValue("odt.colorHeadings", self.colorHeadings.isChecked())
self._build.setValue("odt.scaleHeadings", self.scaleHeadings.isChecked())
self._build.setValue("odt.boldHeadings", self.boldHeadings.isChecked())
self._build.setValue("odt.pageHeader", self.odtPageHeader.text())
self._build.setValue("odt.pageCountOffset", self.odtPageCountOffset.value())
# Documents
self._build.setValue("doc.colorHeadings", self.colorHeadings.isChecked())
self._build.setValue("doc.scaleHeadings", self.scaleHeadings.isChecked())
self._build.setValue("doc.boldHeadings", self.boldHeadings.isChecked())
self._build.setValue("doc.pageHeader", self.odtPageHeader.text())
self._build.setValue("doc.pageCountOffset", self.odtPageCountOffset.value())
# HTML Document
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
@@ -1537,7 +1537,7 @@ class _FormattingTab(NScrollableForm):
def _resetPageHeader(self) -> None:
"""Reset the ODT header format to default."""
self.odtPageHeader.setText(nwHeadFmt.ODT_AUTO)
self.odtPageHeader.setText(nwHeadFmt.DOC_AUTO)
self.odtPageHeader.setCursorPosition(0)
return
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.5.2" hexVersion="0x020502f0" fileVersion="1.5" fileRevision="4" timeStamp="2024-09-17 15:33:20">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1956" autoCount="277" editTime="91001">
<novelWriterXML appVersion="2.6a1" hexVersion="0x020600a1" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-19 20:03:34">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2073" autoCount="277" editTime="93036">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -46,7 +46,7 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H0" charCount="233" wordCount="47" paraCount="2" cursorPos="275" />
<meta expanded="no" heading="H0" charCount="233" wordCount="47" paraCount="2" cursorPos="30" />
<name status="sf12341" import="ia857f0" active="yes">Page</name>
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2953" wordCount="520" paraCount="15" cursorPos="0" />
<meta expanded="no" heading="H3" charCount="2953" wordCount="520" paraCount="15" cursorPos="66" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -66,7 +66,7 @@
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="0" />
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="357" />
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Types xmlns:ns0="http://schemas.openxmlformats.org/package/2006/content-types">
<ns0:Default Extension="xml" ContentType="application/xml" />
<ns0:Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<ns0:Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<ns0:Override PartName="/word/_rels/document.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<ns0:Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
<ns0:Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" />
<ns0:Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" />
<ns0:Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" />
<ns0:Override PartName="/word/header2.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" />
<ns0:Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" />
<ns0:Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" />
<ns0:Override PartName="/word/footnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" />
</ns0:Types>
@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Properties xmlns:ns0="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<ns0:TotalTime>36</ns0:TotalTime>
<ns0:Application>novelWriter/2.6a1</ns0:Application>
<ns0:Words>4029</ns0:Words>
<ns0:Characters>21251</ns0:Characters>
<ns0:CharactersWithSpaces>24914</ns0:CharactersWithSpaces>
<ns0:Paragraphs>42</ns0:Paragraphs>
</ns0:Properties>
@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2024-10-21T13:54:49</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2024-10-21T13:54:49</dcterms:modified>
<dc:creator>lipsum.com</dc:creator>
<dc:title>Lorem Ipsum</dc:title>
<dc:creator>lipsum.com</dc:creator>
<dc:language>en_GB</dc:language>
<cp:revision>45</cp:revision>
<cp:lastModifiedBy>lipsum.com</cp:lastModifiedBy>
</coreProperties>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Relationships xmlns:ns0="http://schemas.openxmlformats.org/package/2006/relationships">
<ns0:Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml" />
<ns0:Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml" />
<ns0:Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header2.xml" />
<ns0:Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml" />
<ns0:Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml" />
</ns0:Relationships>
@@ -0,0 +1,20 @@
<?xml version='1.0' encoding='utf-8'?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnote w:id="1">
<w:p>
<w:pPr>
<w:pStyle w:val="FootnoteText" />
</w:pPr>
<w:r>
<w:rPr>
<w:i />
</w:rPr>
<w:t>Lorem ipsum</w:t>
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin. (Source: Wikipedia)</w:t>
</w:r>
</w:p>
</w:footnote>
</w:footnotes>
@@ -0,0 +1,38 @@
<?xml version='1.0' encoding='utf-8'?>
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<w:pPr>
<w:pStyle w:val="Header" />
<w:jc w:val="right" />
<w:rPr />
</w:pPr>
<w:r>
<w:rPr />
<w:t xml:space="preserve">Page </w:t>
</w:r>
<w:r>
<w:rPr />
<w:fldChar w:fldCharType="begin" />
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> PAGE </w:t>
</w:r>
<w:r>
<w:rPr />
<w:fldChar w:fldCharType="separate" />
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve">2</w:t>
</w:r>
<w:r>
<w:rPr />
<w:fldChar w:fldCharType="end" />
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> - Lorem Ipsum (lipsum.com)</w:t>
</w:r>
</w:p>
</w:hdr>
@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<w:pPr>
<w:pStyle w:val="Header" />
<w:jc w:val="right" />
<w:rPr />
</w:pPr>
<w:r>
<w:rPr />
</w:r>
</w:p>
</w:hdr>
@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Relationships xmlns:ns0="http://schemas.openxmlformats.org/package/2006/relationships">
<ns0:Relationship Id="rId1" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml" />
<ns0:Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml" />
<ns0:Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml" />
</ns0:Relationships>
@@ -0,0 +1,19 @@
<?xml version='1.0' encoding='utf-8'?>
<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnotePr>
<w:numFmt w:val="decimal" />
</w:footnotePr>
<w:docVars>
<w:docVar w:name="ManuscriptTitleCount" w:val="11" />
<w:docVar w:name="ManuscriptParagraphCount" w:val="42" />
<w:docVar w:name="ManuscriptAllWords" w:val="4029" />
<w:docVar w:name="ManuscriptTextWords" w:val="3705" />
<w:docVar w:name="ManuscriptTitleWords" w:val="21" />
<w:docVar w:name="ManuscriptAllChars" w:val="27014" />
<w:docVar w:name="ManuscriptTextChars" w:val="24914" />
<w:docVar w:name="ManuscriptTitleChars" w:val="123" />
<w:docVar w:name="ManuscriptAllWordChars" w:val="23075" />
<w:docVar w:name="ManuscriptTextWordChars" w:val="21251" />
<w:docVar w:name="ManuscriptTitleWordChars" w:val="113" />
</w:docVars>
</w:settings>
@@ -0,0 +1,153 @@
<?xml version='1.0' encoding='utf-8'?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:docDefaults>
<w:rPrDefault>
<w:rPr>
<w:rFonts w:ascii="Source Sans Pro" w:hAnsi="Source Sans Pro" w:cs="Source Sans Pro" />
<w:sz w:val="24" />
<w:szCs w:val="24" />
<w:lang w:val="en_GB" />
</w:rPr>
</w:rPrDefault>
<w:pPrDefault>
<w:pPr>
<w:spacing w:line="276" />
</w:pPr>
</w:pPrDefault>
</w:docDefaults>
<w:style w:type="paragraph" w:styleId="Normal" w:default="1">
<w:name w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="139" w:line="276" />
<w:jc w:val="left" />
</w:pPr>
<w:rPr>
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Title">
<w:name w:val="Title" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="340" w:after="120" w:line="690" />
<w:outlineLvl w:val="0" />
</w:pPr>
<w:rPr>
<w:b />
<w:sz w:val="60" />
<w:szCs w:val="60" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="Heading 1" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="340" w:after="120" w:line="552" />
<w:outlineLvl w:val="0" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="2a6099" />
<w:sz w:val="48" />
<w:szCs w:val="48" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading2">
<w:name w:val="Heading 2" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="400" w:after="120" w:line="483" />
<w:outlineLvl w:val="1" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="2a6099" />
<w:sz w:val="42" />
<w:szCs w:val="42" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading3">
<w:name w:val="Heading 3" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="280" w:after="120" w:line="414" />
<w:outlineLvl w:val="1" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="444444" />
<w:sz w:val="36" />
<w:szCs w:val="36" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading4">
<w:name w:val="Heading 4" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="280" w:after="120" w:line="345" />
<w:outlineLvl w:val="1" />
</w:pPr>
<w:rPr>
<w:b />
<w:color w:val="444444" />
<w:sz w:val="30" />
<w:szCs w:val="30" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Separator">
<w:name w:val="Separator" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="280" w:after="280" w:line="276" />
<w:jc w:val="center" />
</w:pPr>
<w:rPr>
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="MetaText">
<w:name w:val="Meta Text" />
<w:basedOn w:val="Normal" />
<w:next w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="139" w:line="276" />
</w:pPr>
<w:rPr>
<w:color w:val="813709" />
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Header">
<w:name w:val="Header" />
<w:basedOn w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="0" w:line="240" />
<w:jc w:val="right" />
</w:pPr>
<w:rPr>
<w:sz w:val="24" />
<w:szCs w:val="24" />
</w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="FootnoteText">
<w:name w:val="Footnote Text" />
<w:basedOn w:val="Normal" />
<w:pPr>
<w:spacing w:before="0" w:after="90" w:line="220" />
<w:ind w:left="272" w:hanging="272" />
</w:pPr>
<w:rPr>
<w:sz w:val="19" />
<w:szCs w:val="19" />
</w:rPr>
</w:style>
</w:styles>
+35 -5
View File
@@ -33,11 +33,12 @@ from PyQt5.QtGui import QColor, QDesktopServices, QFontDatabase
from novelwriter.common import (
NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, compact, cssCol, describeFont,
elide, formatFileFilter, formatInt, formatTime, formatTimeStamp,
formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode,
makeFileNameSafe, minmax, numberToRoman, openExternalPath, readTextFile,
simplified, transferCase, uniqueCompact, xmlIndent, yesNo
elide, firstFloat, formatFileFilter, formatInt, formatTime,
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle,
isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag,
jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath,
readTextFile, simplified, transferCase, uniqueCompact, xmlIndent,
xmlSubElem, yesNo
)
from tests.mocked import causeOSError
@@ -291,6 +292,15 @@ def testBaseCommon_checkIntTuple():
assert checkIntTuple(5, (0, 1, 2), 3) == 3
@pytest.mark.base
def testBaseCommon_firstFloat():
"""Test the firstFloat function."""
assert firstFloat(None, 1.0) == 1.0
assert firstFloat(1.0, None) == 1.0
assert firstFloat(None, 1) == 0.0
assert firstFloat(None, "1.0") == 0.0
@pytest.mark.base
def testBaseCommon_formatTimeStamp():
"""Test the formatTimeStamp function."""
@@ -624,6 +634,26 @@ def testBaseCommon_xmlIndent():
assert data == "foobar"
@pytest.mark.base
def testBaseCommon_xmlSubElem():
"""Test the xmlSubElem function."""
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", None, attrib={"a": "b"})
) == b'<node a="b" />'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})
) == b'<node a="b">text</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})
) == b'<node a="b">42</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", 3.14, attrib={"a": "b"})
) == b'<node a="b">3.14</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", True, attrib={"a": "b"})
) == b'<node a="b">true</node>'
@pytest.mark.base
def testBaseCommon_readTextFile(monkeypatch, fncPath, ipsumText):
"""Test the readTextFile function."""
+1 -1
View File
@@ -148,7 +148,7 @@ def testCoreBuildSettings_BuildValues():
build = BuildSettings()
strSetting = "headings.fmtPart"
intSetting = "odt.pageCountOffset"
intSetting = "doc.pageCountOffset"
boolSetting = "filter.includeNovel"
floatSetting = "format.lineHeight"
+1 -1
View File
@@ -66,7 +66,7 @@ BUILD_CONF = {
"format.stripUnicode": False,
"format.replaceTabs": True,
"format.firstLineIndent": True,
"odt.colorHeadings": True,
"doc.colorHeadings": True,
"html.addStyles": True,
},
"content": {
+566
View File
@@ -0,0 +1,566 @@
"""
novelWriter ToDocX Class Tester
=================================
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 xml.etree.ElementTree as ET
import zipfile
import pytest
from novelwriter.common import xmlIndent
from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.project import NWProject
from novelwriter.enum import nwBuildFmt
from novelwriter.formats.todocx import (
S_FNOTE, S_HEAD1, S_HEAD2, S_HEAD3, S_HEAD4, S_META, S_NORM, S_SEP,
S_TITLE, ToDocX, _mkTag, _wTag
)
from tests.tools import DOCX_IGNORE, cmpFiles
OOXML_SCM = "http://schemas.openxmlformats.org"
XML_NS = [
f' xmlns:r="{OOXML_SCM}/officeDocument/2006/relationships"',
f' xmlns:w="{OOXML_SCM}/wordprocessingml/2006/main"',
f' xmlns:cp="{OOXML_SCM}/package/2006/metadata/core-properties"',
' xmlns:dc="http://purl.org/dc/elements/1.1/"',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
' xmlns:xml="http://www.w3.org/XML/1998/namespace"',
' xmlns:dcterms="http://purl.org/dc/terms/"',
]
def xmlToText(xElem):
"""Get the text content of an XML element."""
rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for ns in XML_NS:
rTxt = rTxt.replace(ns, "")
return rTxt
@pytest.mark.core
def testFmtToDocX_ParagraphStyles(mockGUI):
"""Test formatting of paragraphs."""
project = NWProject()
doc = ToDocX(project)
doc.setSynopsis(True)
doc.setComments(True)
doc.setKeywords(True)
doc.initDocument()
# Normal Text
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Title
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TITLE, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_TITLE}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 1
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD1, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD1}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 2
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD2, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD2}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 3
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD3, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD3}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 4
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_HEAD4, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD4}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Separator
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SEP, 0, "* * *", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_SEP}" /></w:pPr><w:r><w:rPr />'
'<w:t>* * *</w:t></w:r></w:p></w:body>'
)
# Empty Paragraph
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SKIP, 0, "* * *", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr></w:p></w:body>'
)
# Synopsis
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SYNOPSIS, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Synopsis:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Short
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_SHORT, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Short Description:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Comment
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_COMMENT, 0, "Hello World", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Comment:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Tags and References (Single)
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_KEYWORD, 0, "tag: Stuff", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Tag:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Stuff</w:t></w:r>'
'</w:p></w:body>'
)
# Tags and References (Multiple)
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_KEYWORD, 0, "char: Jane, John", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>Characters:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> Jane, John</w:t></w:r>'
'</w:p></w:body>'
)
# Tags and References (Invalid)
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_KEYWORD, 0, "stuff: Stuff", [], doc.A_NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr></w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_ParagraphFormatting(mockGUI):
"""Test formatting of paragraphs."""
project = NWProject()
doc = ToDocX(project)
doc.setSynopsis(True)
doc.setComments(True)
doc.setKeywords(True)
doc.initDocument()
# Left Align
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_LEFT)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="left" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Right Align
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_RIGHT)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="right" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Center Align
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_CENTRE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="center" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Justify
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_JUSTIFY)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="both" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Page Break Before
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_PBB)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'</w:p></w:body>'
)
# Page Break After
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_PBA)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'<w:r><w:br w:type="page" /></w:r>'
'</w:p></w:body>'
)
# Zero Margins
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_Z_TOPMRG | doc.A_Z_BTMMRG)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:spacing w:before="0" w:after="0" w:line="252" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Indent
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_IND_L | doc.A_IND_R)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:ind w:left="880" w:right="880" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# First Line Indent
xTest = ET.Element(_wTag("body"))
doc._tokens = [(doc.T_TEXT, 0, "Hello World", [], doc.A_IND_T)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:ind w:firstLine="308" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_TextFormatting(mockGUI):
"""Test formatting of text."""
project = NWProject()
doc = ToDocX(project)
doc.initDocument()
# Markdown
xTest = ET.Element(_wTag("body"))
doc._text = "Text **bold**, _italic_, ~~strike~~."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Text </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>bold</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
'<w:r><w:rPr><w:i /></w:rPr><w:t>italic</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t>strike</w:t></w:r>'
'<w:r><w:rPr /><w:t>.</w:t></w:r>'
'</w:p></w:body>'
)
# Nested Shortcode Text, Emphasis
xTest = ET.Element(_wTag("body"))
doc._text = "Some [s]nested [b]bold[/b] [u]and[/u] [i]italics[/i] text[/s] text."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve">nested </w:t></w:r>'
'<w:r><w:rPr><w:b /><w:strike /></w:rPr><w:t>bold</w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /><w:strike /></w:rPr><w:t>and</w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:i /><w:strike /></w:rPr><w:t>italics</w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve"> text</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> text.</w:t></w:r>'
'</w:p></w:body>'
)
# Shortcode Text, Super/Subscript
xTest = ET.Element(_wTag("body"))
doc._text = "Some super[sup]script[/sup] and sub[sub]script[/sub] text."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Some super</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr><w:t>script</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> and sub</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="subscript" /></w:rPr><w:t>script</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> text.</w:t></w:r>'
'</w:p></w:body>'
)
# Shortcode Text, Underline/Highlight
xTest = ET.Element(_wTag("body"))
doc._text = "Some [u]underlined and [m]highlighted[/m][/u] text."
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /></w:rPr>'
'<w:t xml:space="preserve">underlined and </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /><w:shd w:fill="ffffa6" w:val="clear" /></w:rPr>'
'<w:t>highlighted</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> text.</w:t></w:r>'
'</w:p></w:body>'
)
# Hard Break
xTest = ET.Element(_wTag("body"))
doc._text = "Some text.\nNext line\n"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Some text.</w:t><w:br /><w:t>Next line</w:t></w:r>'
'</w:p></w:body>'
)
# Tab
xTest = ET.Element(_wTag("body"))
doc._text = "\tItem 1\tItem 2\n"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:tab /><w:t>Item 1</w:t><w:tab /><w:t>Item 2</w:t></w:r>'
'</w:p></w:body>'
)
# Tab in Format
xTest = ET.Element(_wTag("body"))
doc._text = "Some **bold\ttext**"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>bold</w:t><w:tab /><w:t>text</w:t></w:r>'
'</w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_Footnotes(mockGUI):
"""Test formatting of footnotes."""
project = NWProject()
doc = ToDocX(project)
doc.initDocument()
# Text
xTest = ET.Element(_wTag("body"))
doc._text = (
"Text with one[footnote:fa], **two**[footnote:fd], "
"or three[footnote:fb] footnotes.[footnote:fe]\n\n"
"%footnote.fa: Footnote text A.[footnote:fc]\n\n"
"%footnote.fc: This footnote is skipped.\n\n"
"%footnote.fd: Another footnote.\n\n"
"%footnote.fe: Again?\n\n"
)
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Text with one</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="1" /></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>two</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="2" /></w:r>'
'<w:r><w:rPr /><w:t>, or three</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> footnotes.</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="3" /></w:r>'
'</w:p></w:body>'
)
# Footnotes
doc._footnotesXml()
assert xmlToText(doc._files["footnotes.xml"].xml) == (
'<w:footnotes>'
f'<w:footnote w:id="1"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Footnote text A.</w:t></w:r></w:p></w:footnote>'
f'<w:footnote w:id="2"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Another footnote.</w:t></w:r></w:p></w:footnote>'
f'<w:footnote w:id="3"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:r><w:rPr /><w:t>Again?</w:t></w:r></w:p></w:footnote>'
'</w:footnotes>'
)
@pytest.mark.core
def testFmtToDocX_SaveDocument(mockGUI, prjLipsum, fncPath, tstPaths):
"""Test document output."""
project = NWProject()
project.openProject(prjLipsum)
pageHeader = f"Page {nwHeadFmt.DOC_PAGE} - {nwHeadFmt.DOC_PROJECT} ({nwHeadFmt.DOC_AUTHOR})"
build = BuildSettings()
build.setValue("filter.includeNovel", True)
build.setValue("filter.includeNotes", True)
build.setValue("filter.includeInactive", False)
build.setValue("text.includeSynopsis", True)
build.setValue("text.includeComments", True)
build.setValue("text.includeKeywords", True)
build.setValue("format.textFont", "Source Sans Pro,12")
build.setValue("format.firstLineIndent", True)
build.setValue("doc.pageHeader", pageHeader)
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
docPath = fncPath / "document.docx"
assert list(docBuild.iterBuildDocument(docPath, nwBuildFmt.DOCX)) == [
(0, True), (1, True), (2, True), (3, True), (4, True), (5, False),
(6, True), (7, True), (8, True), (9, False), (10, False), (11, True),
(12, True), (13, True), (14, True), (15, True), (16, True), (17, True),
(18, True), (19, True), (20, True),
]
assert docPath.exists()
assert zipfile.is_zipfile(docPath)
with zipfile.ZipFile(docPath, mode="r") as zipObj:
zipObj.extractall(fncPath / "extract")
def prettifyXml(inFile, outFile):
with open(outFile, mode="wb") as fStream:
xml = ET.parse(inFile)
xmlIndent(xml)
xml.write(fStream, encoding="utf-8", xml_declaration=True)
expected = [
fncPath / "extract" / "[Content_Types].xml",
fncPath / "extract" / "_rels" / ".rels",
fncPath / "extract" / "docProps" / "app.xml",
fncPath / "extract" / "docProps" / "core.xml",
fncPath / "extract" / "word" / "_rels" / "document.xml.rels",
fncPath / "extract" / "word" / "document.xml",
fncPath / "extract" / "word" / "footnotes.xml",
fncPath / "extract" / "word" / "header1.xml",
fncPath / "extract" / "word" / "header2.xml",
fncPath / "extract" / "word" / "settings.xml",
fncPath / "extract" / "word" / "styles.xml",
]
outDir = tstPaths.outDir / "fmtToDocX_SaveDocument"
outDir.mkdir()
for file in expected:
assert file.is_file()
name = file.name.replace("[", "").replace("]", "").lstrip(".")
outFile = outDir / name
refFile = tstPaths.refDir / f"fmtToDocX_SaveDocument_{name}"
prettifyXml(file, outFile)
assert cmpFiles(outFile, refFile, ignoreStart=DOCX_IGNORE)
@pytest.mark.core
def testFmtToDocX_MkTag():
"""Test the tag maker function."""
assert _mkTag("r", "id") == f"{{{OOXML_SCM}/officeDocument/2006/relationships}}id"
assert _mkTag("w", "t") == f"{{{OOXML_SCM}/wordprocessingml/2006/main}}t"
assert _mkTag("q", "t") == "t"
+23 -7
View File
@@ -1,6 +1,6 @@
"""
novelWriter ToOdt Class Tester
=================================
================================
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
@@ -48,8 +48,8 @@ XML_NS = [
def xmlToText(xElem):
"""Get the text content of an XML element."""
rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for nSpace in XML_NS:
rTxt = rTxt.replace(nSpace, "")
for ns in XML_NS:
rTxt = rTxt.replace(ns, "")
return rTxt
@@ -623,7 +623,7 @@ def testFmtToOdt_ConvertParagraphs(mockGUI):
'<office:text>'
'<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
'<text:p text:style-name="Text_20_body">Regular paragraph</text:p>'
'<text:p text:style-name="Text_20_body">with<text:line-break />break</text:p>'
'<text:p text:style-name="P7">with<text:line-break />break</text:p>'
'<text:p text:style-name="P7">Left Align</text:p>'
'</office:text>'
)
@@ -772,8 +772,8 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
assert odt._dLanguage == ""
odt.setLanguage("nb_NO")
assert odt._dLanguage == "nb"
odt.setHeaderFormat(nwHeadFmt.ODT_AUTO, 1)
assert odt._headerFormat == nwHeadFmt.ODT_AUTO
odt.setHeaderFormat(nwHeadFmt.DOC_AUTO, 1)
assert odt._headerFormat == nwHeadFmt.DOC_AUTO
odt.setFirstLineIndent(True, 1.4, False)
assert odt._firstIndent is True
assert odt._fTextIndent == "0.499cm"
@@ -822,7 +822,7 @@ def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths):
odt._isNovel = True
# Set a format without page number
odt.setHeaderFormat(f"{nwHeadFmt.ODT_PROJECT} - {nwHeadFmt.ODT_AUTHOR}", 0)
odt.setHeaderFormat(f"{nwHeadFmt.DOC_PROJECT} - {nwHeadFmt.DOC_AUTHOR}", 0)
odt._text = (
"## Chapter One\n\n"
@@ -1127,6 +1127,22 @@ def testFmtToOdt_ODTParagraphStyle():
'</test>'
)
# Override Justify
# ================
aStyle = ODTParagraphStyle("test")
# When not set, override is possible
assert aStyle._pAttr["text-align"][1] is None
aStyle.overrideJustify("left")
assert aStyle._pAttr["text-align"][1] == "left"
# When explicitly set, not override
aStyle.setTextAlign("right")
assert aStyle._pAttr["text-align"][1] == "right"
aStyle.overrideJustify("left")
assert aStyle._pAttr["text-align"][1] == "right"
# Changes
# =======
+12 -12
View File
@@ -707,11 +707,11 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI):
"""Test the format-specific settings."""
build = BuildSettings()
build.setValue("odt.pageHeader", nwHeadFmt.ODT_AUTO)
build.setValue("odt.pageCountOffset", 0)
build.setValue("odt.colorHeadings", True)
build.setValue("odt.scaleHeadings", True)
build.setValue("odt.boldHeadings", True)
build.setValue("doc.pageHeader", nwHeadFmt.DOC_AUTO)
build.setValue("doc.pageCountOffset", 0)
build.setValue("doc.colorHeadings", True)
build.setValue("doc.scaleHeadings", True)
build.setValue("doc.boldHeadings", True)
build.setValue("html.addStyles", False)
build.setValue("html.preserveTabs", False)
@@ -726,7 +726,7 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI):
assert bSettings.toolStack.currentWidget() is fmtTab
# Check initial values
assert fmtTab.odtPageHeader.text() == nwHeadFmt.ODT_AUTO
assert fmtTab.odtPageHeader.text() == nwHeadFmt.DOC_AUTO
assert fmtTab.odtPageCountOffset.value() == 0
assert fmtTab.colorHeadings.isChecked() is True
assert fmtTab.scaleHeadings.isChecked() is True
@@ -749,18 +749,18 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI):
# Save values
fmtTab.saveContent()
assert build.getStr("odt.pageHeader") == "Stuff"
assert build.getInt("odt.pageCountOffset") == 1
assert build.getBool("odt.colorHeadings") is False
assert build.getBool("odt.scaleHeadings") is False
assert build.getBool("odt.boldHeadings") is False
assert build.getStr("doc.pageHeader") == "Stuff"
assert build.getInt("doc.pageCountOffset") == 1
assert build.getBool("doc.colorHeadings") is False
assert build.getBool("doc.scaleHeadings") is False
assert build.getBool("doc.boldHeadings") is False
assert build.getBool("html.addStyles") is True
assert build.getBool("html.preserveTabs") is True
# Reset header format
fmtTab.btnPageHeader.click()
assert fmtTab.odtPageHeader.text() == nwHeadFmt.ODT_AUTO
assert fmtTab.odtPageHeader.text() == nwHeadFmt.DOC_AUTO
# Finish
bSettings._dialogButtonClicked(bSettings.buttonBox.button(QtDialogClose))
+3 -1
View File
@@ -30,6 +30,7 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QWidget
XML_IGNORE = ("<novelWriterXML", "<project")
ODT_IGNORE = ("<meta:generator", "<meta:creation-date", "<dc:date", "<meta:editing")
NWD_IGNORE = ("%%~date:",)
DOCX_IGNORE = ("<dcterms:created", "<dcterms:modified")
MOCK_TIME = datetime(2019, 5, 10, 18, 52, 0).timestamp()
@@ -59,7 +60,8 @@ class C:
def cmpFiles(
fileOne: str | Path, fileTwo: str | Path,
fileOne: str | Path,
fileTwo: str | Path,
ignoreLines: list | None = None,
ignoreStart: tuple | None = None
) -> bool: