Add DocX support (#2056)
This commit is contained in:
@@ -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
|
||||
##
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
@@ -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((
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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."""
|
||||
|
||||
@@ -148,7 +148,7 @@ def testCoreBuildSettings_BuildValues():
|
||||
build = BuildSettings()
|
||||
|
||||
strSetting = "headings.fmtPart"
|
||||
intSetting = "odt.pageCountOffset"
|
||||
intSetting = "doc.pageCountOffset"
|
||||
boolSetting = "filter.includeNovel"
|
||||
floatSetting = "format.lineHeight"
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
"""
|
||||
novelWriter – ToDocX Class Tester
|
||||
=================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, 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"
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
novelWriter – ToOdt Class Tester
|
||||
=================================
|
||||
================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, 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
|
||||
# =======
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user