diff --git a/nw/core/__init__.py b/nw/core/__init__.py
index f5140ebc..fc90379b 100644
--- a/nw/core/__init__.py
+++ b/nw/core/__init__.py
@@ -5,6 +5,7 @@ from nw.core.index import NWIndex
from nw.core.project import NWProject
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
from nw.core.tohtml import ToHtml
+from nw.core.toodt import ToOdt
from nw.core.tools import countWords, numberToRoman, numberToWord
__all__ = [
@@ -18,4 +19,5 @@ __all__ = [
"NWSpellEnchant",
"NWSpellSimple",
"ToHtml",
+ "ToOdt",
]
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 46c7d8b6..1a5b345c 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -61,6 +61,8 @@ class ToHtml(Tokenizer):
self.reReverse = []
self._buildRegEx()
+ self.fullHTML = []
+
return
##
@@ -90,6 +92,11 @@ class ToHtml(Tokenizer):
# Class Methods
##
+ def getFullResultSize(self):
+ """Return the size of the full HTML result.
+ """
+ return sum([len(x) for x in self.fullHTML])
+
def doAutoReplace(self):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
@@ -108,9 +115,12 @@ class ToHtml(Tokenizer):
if self.genMode == self.M_PREVIEW:
# Doesn't matter for preview as we don't use the markdown
return
- self.theMarkdown = self.reReverse.sub(
- lambda x: self.revDict[x.group(0)], self.theMarkdown
- )
+
+ if self.keepMarkdown:
+ self.theMarkdown[-1] = self.reReverse.sub(
+ lambda x: self.revDict[x.group(0)], self.theMarkdown[-1]
+ )
+
return
def doConvert(self):
@@ -165,24 +175,27 @@ class ToHtml(Tokenizer):
if tStyle is not None and self.cssStyles:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
- if tStyle & self.A_RIGHT:
+ elif tStyle & self.A_RIGHT:
aStyle.append("text-align: right;")
- if tStyle & self.A_CENTRE:
+ elif tStyle & self.A_CENTRE:
aStyle.append("text-align: center;")
- if tStyle & self.A_JUSTIFY:
+ elif tStyle & self.A_JUSTIFY:
aStyle.append("text-align: justify;")
+
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
- if tStyle & self.A_PBB_AV:
- aStyle.append("page-break-before: avoid;")
- if tStyle & self.A_PBB_NO:
- aStyle.append("page-break-before: never;")
+ elif tStyle & self.A_PBB_AUT:
+ aStyle.append("page-break-before: auto;")
+
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
- if tStyle & self.A_PBA_AV:
- aStyle.append("page-break-after: avoid;")
- if tStyle & self.A_PBA_NO:
- aStyle.append("page-break-after: never;")
+ elif tStyle & self.A_PBA_AUT:
+ aStyle.append("page-break-after: auto;")
+
+ if tStyle & self.A_Z_BTMMRG:
+ aStyle.append("margin-bottom: 0;")
+ if tStyle & self.A_Z_TOPMRG:
+ aStyle.append("margin-top: 0;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
@@ -240,12 +253,12 @@ class ToHtml(Tokenizer):
if parStyle is None:
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
- tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
+ tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
- thisPar.append(tTemp.rstrip()+"
")
+ thisPar.append(tTemp.rstrip() + "
")
hasHardBreak = True
else:
- thisPar.append(tTemp.rstrip()+" ")
+ thisPar.append(tTemp.rstrip() + " ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
@@ -254,11 +267,60 @@ class ToHtml(Tokenizer):
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
- tmpResult.append(self._formatKeywords(tText))
+ tTemp = "
%s
\n" % (hStyle, self._formatKeywords(tText))
+ tmpResult.append(tTemp)
self.theResult = "".join(tmpResult)
tmpResult = []
+ if self.genMode != self.M_PREVIEW:
+ self.fullHTML.append(self.theResult)
+
+ return
+
+ def saveHTML5(self, savePath):
+ """Save the data to an .html file.
+ """
+ with open(savePath, mode="w", encoding="utf8") as outFile:
+ theStyle = self.getStyleSheet()
+ theStyle.append("article {width: 800px; margin: 40px auto;}")
+ bodyText = "".join(self.fullHTML)
+ bodyText = bodyText.replace("\t", " ").rstrip()
+
+ theHtml = (
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "{projTitle:s}\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "{bodyText:s}\n"
+ "\n"
+ "\n"
+ "\n"
+ ).format(
+ projTitle = self.theProject.projName,
+ htmlStyle = "\n".join(theStyle),
+ bodyText = bodyText,
+ )
+ outFile.write(theHtml)
+
+ return
+
+ def replaceTabs(self, nSpaces=8, spaceChar=" "):
+ """Replace tabs with spaces in the html.
+ """
+ htmlText = []
+ eightSpace = spaceChar*nSpaces
+ for aLine in self.fullHTML:
+ htmlText.append(aLine.replace("\t", eightSpace))
+
+ self.fullHTML = htmlText
return
def getStyleSheet(self):
@@ -268,22 +330,23 @@ class ToHtml(Tokenizer):
if not self.cssStyles:
return theStyles
- if self.doJustify:
- theStyles.append(r"p {text-align: justify;}")
- else:
- theStyles.append(r"p {text-align: left;}")
+ textAlign = "justify" if self.doJustify else "left"
- theStyles.append(r"h1, h2 {color: rgb(66, 113, 174);}")
- theStyles.append(r"h3, h4 {color: rgb(50, 50, 50);}")
- theStyles.append(r"h1, h2, h3, h4 {page-break-after: avoid;}")
- theStyles.append(r"a {color: rgb(66, 113, 174);}")
- theStyles.append(r".title {font-size: 2.5em;}")
- theStyles.append(r".tags {color: rgb(245, 135, 31); font-weight: bold;}")
- theStyles.append(r".break {text-align: left;}")
- theStyles.append(r".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}")
- theStyles.append(r".skip {margin-top: 1em; margin-bottom: 1em;}")
- theStyles.append(r".synopsis {font-style: italic;}")
- theStyles.append(r".comment {font-style: italic; color: rgb(100, 100, 100);}")
+ theStyles.append("body {font-family: '%s'; font-size: %dpt}" % (
+ self.textFont, self.textSize)
+ )
+ theStyles.append("p {text-align: %s;}" % textAlign)
+ theStyles.append("h1, h2 {color: rgb(66, 113, 174);}")
+ theStyles.append("h3, h4 {color: rgb(50, 50, 50);}")
+ theStyles.append("h1, h2, h3, h4 {page-break-after: avoid;}")
+ theStyles.append("a {color: rgb(66, 113, 174);}")
+ theStyles.append(".title {font-size: 2.5em;}")
+ theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
+ theStyles.append(".break {text-align: left;}")
+ theStyles.append(".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}")
+ theStyles.append(".skip {margin-top: 1em; margin-bottom: 1em;}")
+ theStyles.append(".synopsis {font-style: italic;}")
+ theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
return theStyles
@@ -337,7 +400,7 @@ class ToHtml(Tokenizer):
))
retText += ", ".join(refTags)
- return "%s
\n" % retText
+ return retText
def _buildRegEx(self):
"""Build the regular expressions
diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py
index 6fa7a87a..a1ffe11f 100644
--- a/nw/core/tokenizer.py
+++ b/nw/core/tokenizer.py
@@ -67,11 +67,11 @@ class Tokenizer():
A_CENTRE = 0x0004 # Centred
A_JUSTIFY = 0x0008 # Justified
A_PBB = 0x0010 # Page break before always
- A_PBB_AV = 0x0020 # Page break before avoid
- A_PBB_NO = 0x0040 # Page break before never
- A_PBA = 0x0080 # Page break after always
- A_PBA_AV = 0x0100 # Page break after avoid
- A_PBA_NO = 0x0200 # Page break after avoid
+ A_PBB_AUT = 0x0020 # Page break before auto
+ A_PBA = 0x0040 # Page break after always
+ A_PBA_AUT = 0x0080 # Page break after auto
+ A_Z_TOPMRG = 0x0100 # Zero top margin
+ A_Z_BTMMRG = 0x0200 # Zero bottom margin
def __init__(self, theProject, theParent):
@@ -79,20 +79,36 @@ class Tokenizer():
self.theParent = theParent
# Data Variables
- self.theText = None # The raw text to be tokenized
+ self.theText = "" # The raw text to be tokenized
self.theHandle = None # The handle associated with the text
self.theItem = None # The NWItem associated with the handle
- self.theTokens = None # The list of the processed tokens
- self.theResult = None # The result text after conversion
- self.theMarkdown = None # The result text in novelWriter markdown
+ self.theTokens = [] # The list of the processed tokens
+ self.theResult = "" # The result of the last document
+
+ self.keepMarkdown = False # Whether to keep the markdown text
+ self.theMarkdown = [] # The result novelWriter markdown of all documents
# User Settings
- 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.doJustify = False # Justify text
+ self.textFont = "Serif" # Output text font
+ self.textSize = 11 # Output text size
+ self.textFixed = False # Fixed width text
+ self.lineHeight = 1.15 # Line height
+ 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
+ ## Title Margins
+ self.marginTitle = (1.000, 0.500)
+ self.marginHead1 = (1.000, 0.500)
+ self.marginHead2 = (0.834, 0.500)
+ self.marginHead3 = (0.584, 0.500)
+ self.marginHead4 = (0.584, 0.500)
+ self.marginText = (0.000, 0.584)
+ self.marginMeta = (0.000, 0.584)
+
+ ## Title Formats
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
@@ -153,6 +169,48 @@ class Tokenizer():
self.hideSection = hideSection
return
+ def setFont(self, textFont, textSize, textFixed=False):
+ self.textFont = textFont
+ self.textSize = round(int(textSize))
+ self.textFixed = textFixed
+ return
+
+ def setLineHeight(self, lineHeight):
+ self.lineHeight = float(lineHeight)
+ return
+
+ def setJustify(self, doJustify):
+ self.doJustify = doJustify
+ return
+
+ def setTitleMargins(self, mUpper, mLower):
+ self.marginTitle = (float(mUpper), float(mLower))
+ return
+
+ def setHead1Margins(self, mUpper, mLower):
+ self.marginHead1 = (float(mUpper), float(mLower))
+ return
+
+ def setHead2Margins(self, mUpper, mLower):
+ self.marginHead2 = (float(mUpper), float(mLower))
+ return
+
+ def setHead3Margins(self, mUpper, mLower):
+ self.marginHead3 = (float(mUpper), float(mLower))
+ return
+
+ def setHead4Margins(self, mUpper, mLower):
+ self.marginHead4 = (float(mUpper), float(mLower))
+ return
+
+ def setTextMargins(self, mUpper, mLower):
+ self.marginText = (float(mUpper), float(mLower))
+ return
+
+ def setMetaMargins(self, mUpper, mLower):
+ self.marginMeta = (float(mUpper), float(mLower))
+ return
+
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
return
@@ -173,8 +231,8 @@ class Tokenizer():
self.doKeywords = doKeywords
return
- def setJustify(self, doJustify):
- self.doJustify = doJustify
+ def setKeepMarkdown(self, keepMarkdown):
+ self.keepMarkdown = keepMarkdown
return
##
@@ -196,7 +254,8 @@ class Tokenizer():
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
))
- self.theMarkdown = "# %s\n\n" % theTitle
+ if self.keepMarkdown:
+ self.theMarkdown.append("# %s\n\n" % theTitle)
return True
@@ -238,23 +297,6 @@ class Tokenizer():
return True
- def getResult(self):
- """Return the result from the conversion.
- """
- return self.theResult
-
- def getResultSize(self):
- """Return the size of the result from the conversion.
- """
- if self.theResult is None:
- return 0
- return len(self.theResult)
-
- def getFilteredMarkdown(self):
- """Return the novelWriter markdown after the filters have been applied.
- """
- return self.theMarkdown
-
def doAutoReplace(self):
"""Run through the user's auto-replace dictionary.
"""
@@ -307,7 +349,6 @@ class Tokenizer():
]
self.theTokens = []
- self.theMarkdown = ""
tmpMarkdown = []
nLine = 0
for aLine in self.theText.splitlines():
@@ -316,88 +357,61 @@ class Tokenizer():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
- self.T_EMPTY,
- nLine,
- "",
- None,
- self.A_NONE
+ self.T_EMPTY, nLine, "", None, self.A_NONE
))
- tmpMarkdown.append("\n")
+ if self.keepMarkdown:
+ tmpMarkdown.append("\n")
elif aLine[0] == "%":
cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower()
if synTag == "synopsis:":
self.theTokens.append((
- self.T_SYNOPSIS,
- nLine,
- cLine[9:].strip(),
- None,
- self.A_NONE
+ self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
))
- if self.doSynopsis:
+ if self.doSynopsis and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
- self.T_COMMENT,
- nLine,
- aLine[1:].strip(),
- None,
- self.A_NONE
+ self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
))
- if self.doComments:
+ if self.doComments and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
- self.T_KEYWORD,
- nLine,
- aLine[1:].strip(),
- None,
- self.A_NONE
+ self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
))
- if self.doKeywords:
+ if self.doKeywords and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
- self.T_HEAD1,
- nLine,
- aLine[2:].strip(),
- None,
- self.A_NONE
+ self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
))
- tmpMarkdown.append("%s\n" % aLine)
+ if self.keepMarkdown:
+ tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
- self.T_HEAD2,
- nLine,
- aLine[3:].strip(),
- None,
- self.A_NONE
+ self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
))
- tmpMarkdown.append("%s\n" % aLine)
+ if self.keepMarkdown:
+ tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
- self.T_HEAD3,
- nLine,
- aLine[4:].strip(),
- None,
- self.A_NONE
+ self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
))
- tmpMarkdown.append("%s\n" % aLine)
+ if self.keepMarkdown:
+ tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
- self.T_HEAD4,
- nLine,
- aLine[5:].strip(),
- None,
- self.A_NONE
+ self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
))
- tmpMarkdown.append("%s\n" % aLine)
+ if self.keepMarkdown:
+ tmpMarkdown.append("%s\n" % aLine)
else:
if not self.doBodyText:
@@ -420,26 +434,44 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
- self.T_TEXT,
- nLine,
- aLine,
- fmtPos,
- self.A_NONE
+ self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
))
- tmpMarkdown.append("%s\n" % aLine)
+ if self.keepMarkdown:
+ tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
- self.T_EMPTY,
- nLine,
- "",
- None,
- self.A_NONE
+ self.T_EMPTY, nLine, "", None, self.A_NONE
))
- tmpMarkdown.append("\n")
+ if self.keepMarkdown:
+ tmpMarkdown.append("\n")
- self.theMarkdown = "".join(tmpMarkdown)
- tmpMarkdown = []
+ if self.keepMarkdown:
+ self.theMarkdown.append("".join(tmpMarkdown))
+
+ # Second Pass
+ # ===========
+ # Some items need a second pass
+
+ pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
+ nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
+ tCount = len(self.theTokens)
+ for n, tToken in enumerate(self.theTokens):
+
+ if n > 0:
+ pToken = self.theTokens[n-1]
+ if n < tCount - 1:
+ nToken = self.theTokens[n+1]
+
+ if tToken[0] == self.T_KEYWORD:
+ aStyle = tToken[4]
+ if pToken[0] == self.T_KEYWORD:
+ aStyle |= self.A_Z_TOPMRG
+ if nToken[0] == self.T_KEYWORD:
+ aStyle |= self.A_Z_BTMMRG
+ self.theTokens[n] = (
+ tToken[0], tToken[1], tToken[2], tToken[3], aStyle
+ )
return
@@ -454,9 +486,7 @@ class Tokenizer():
# For novel files, we need to handle chapter numbering, scene
# numbering, and scene breaks
if self.isNovel:
- for n in range(len(self.theTokens)):
-
- tToken = self.theTokens[n]
+ for n, tToken in enumerate(self.theTokens):
# In case we see text before a scene, we reset the flag
if tToken[0] == self.T_TEXT:
@@ -468,11 +498,7 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tTemp,
- None,
- self.A_NONE
+ tToken[0], tToken[1], tTemp, None, self.A_NONE
)
elif tToken[0] == self.T_HEAD2:
@@ -490,11 +516,7 @@ class Tokenizer():
# Format the chapter header
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tTemp,
- None,
- self.A_PBB
+ tToken[0], tToken[1], tTemp, None, self.A_PBB
)
# Set scene variables
@@ -511,53 +533,29 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
- self.T_EMPTY,
- tToken[1],
- "",
- None,
- self.A_NONE
+ self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
- self.T_EMPTY,
- tToken[1],
- "",
- None,
- self.A_NONE
+ self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
- self.T_SKIP,
- tToken[1],
- "",
- None,
- self.A_NONE
+ self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
- self.T_EMPTY,
- tToken[1],
- "",
- None,
- self.A_NONE
+ self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
- self.T_SEP,
- tToken[1],
- tTemp,
- None,
- self.A_CENTRE
+ self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tTemp,
- None,
- self.A_NONE
+ tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# Definitely no longer the first scene
@@ -570,35 +568,19 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
- self.T_EMPTY,
- tToken[1],
- "",
- None,
- self.A_NONE
+ self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
- self.T_SKIP,
- tToken[1],
- "",
- None,
- self.A_NONE
+ self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
- self.T_SEP,
- tToken[1],
- tTemp,
- None,
- self.A_CENTRE
+ self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tTemp,
- None,
- self.A_NONE
+ tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# For title page and partitions, we need to centre all text.
@@ -609,28 +591,18 @@ class Tokenizer():
for n, tToken in enumerate(self.theTokens):
if tToken[0] == self.T_HEAD1:
if self.isTitle:
+ aStyle = self.A_PBB_AUT | self.A_CENTRE
self.theTokens[n] = (
- self.T_TITLE,
- tToken[1],
- tToken[2],
- tToken[3],
- self.A_PBB_NO | self.A_CENTRE
+ self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle
)
else:
+ aStyle = self.A_PBB | self.A_CENTRE
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tToken[2],
- tToken[3],
- self.A_PBB | self.A_CENTRE
+ tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
else:
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tToken[2],
- tToken[3],
- self.A_CENTRE
+ tToken[0], tToken[1], tToken[2], tToken[3], self.A_CENTRE
)
# Add a page break after the last entry
@@ -638,11 +610,7 @@ class Tokenizer():
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tToken[2],
- tToken[3],
- tToken[4] | self.A_PBA
+ tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
@@ -651,19 +619,11 @@ class Tokenizer():
for n, tToken in enumerate(self.theTokens):
if n == 0:
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tToken[2],
- tToken[3],
- self.A_LEFT | self.A_PBB
+ tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
- tToken[0],
- tToken[1],
- tToken[2],
- tToken[3],
- self.A_LEFT
+ tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT
)
return True
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
new file mode 100644
index 00000000..936c7faf
--- /dev/null
+++ b/nw/core/toodt.py
@@ -0,0 +1,1171 @@
+# -*- coding: utf-8 -*-
+"""
+novelWriter – ODT Text Converter
+================================
+Extends the Tokenizer class to generate ODT and FODT files
+
+File History:
+Created: 2021-01-26 [1.1rc1]
+
+This file is a part of novelWriter
+Copyright 2018–2021, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+import nw
+import logging
+
+from lxml import etree
+from hashlib import sha256
+from datetime import datetime
+from zipfile import ZipFile
+
+from nw.core.tokenizer import Tokenizer
+from nw.constants import nwLabels, nwKeyWords
+
+logger = logging.getLogger(__name__)
+
+# Main XML NameSpaces
+XML_NS = {
+ "office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
+ "style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+ "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
+ "text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
+ "meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
+ "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+}
+
+# Mimetype and Version
+X_MIME = "application/vnd.oasis.opendocument.text"
+X_VERS = "1.2"
+
+# Text Formatting Tags
+TAG_BR = "{%s}line-break" % XML_NS["text"]
+TAG_TAB = "{%s}tab" % XML_NS["text"]
+TAG_SPAN = "{%s}span" % XML_NS["text"]
+TAG_STNM = "{%s}style-name" % XML_NS["text"]
+
+class ToOdt(Tokenizer):
+
+ X_BLD = 0x01 # Bold format
+ X_ITA = 0x02 # Italic format
+ X_DEL = 0x04 # Strikethrough format
+ X_BRK = 0x08 # Line break
+ X_TAB = 0x10 # Tab
+
+ def __init__(self, theProject, theParent, isFlat):
+ Tokenizer.__init__(self, theProject, theParent)
+
+ self.mainConf = nw.CONFIG
+
+ self._isFlat = isFlat # Flat: .fodt, otherwise .odt
+
+ self._dFlat = None # FODT file XML root
+ self._dCont = None # ODT content.xml root
+ self._dMeta = None # ODT meta.xml root
+ self._dStyl = None # ODT styles.xml root
+
+ self._xMeta = None # Office meta root
+ self._xStyl = None # Office styles root
+ self._xAuto = None # Office auto-styles root
+ self._xBody = None # Office body root
+ self._xText = None # Office text root
+
+ self._mainPara = {} # User-accessible paragraph styles
+ self._autoPara = {} # Auto-generated paragraph styles
+ self._autoText = {} # Auto-generated text styles
+
+ # Properties
+ self.textFont = "Liberation Serif"
+ self.textSize = 12
+ self.textFixed = False
+ self.colourHead = False
+
+ # Internal
+ self._fontFamily = "'Liberation Sans'"
+ self._fontPitch = "variable"
+ self._fSizeTitle = "30pt"
+ self._fSizeHead1 = "24pt"
+ self._fSizeHead2 = "20pt"
+ self._fSizeHead3 = "16pt"
+ self._fSizeHead4 = "14pt"
+ self._fSizeHead = "14pt"
+ self._fSizeText = "12pt"
+ self._lineHeight = "115%"
+ self._textAlign = "left"
+ self._dLanguage = "en"
+ self._dCountry = "GB"
+
+ ## Text Margings in Units of em
+ self._mTopTitle = "0.423cm"
+ self._mTopHead1 = "0.423cm"
+ self._mTopHead2 = "0.353cm"
+ self._mTopHead3 = "0.247cm"
+ self._mTopHead4 = "0.247cm"
+ self._mTopHead = "0.423cm"
+ self._mTopText = "0.000cm"
+ self._mTopMeta = "0.000cm"
+
+ self._mBotTitle = "0.212cm"
+ self._mBotHead1 = "0.212cm"
+ self._mBotHead2 = "0.212cm"
+ self._mBotHead3 = "0.212cm"
+ self._mBotHead4 = "0.212cm"
+ self._mBotHead = "0.212cm"
+ self._mBotText = "0.247cm"
+ self._mBotMeta = "0.106cm"
+
+ ## Colour
+ self._colHead12 = None
+ self._opaHead12 = None
+ self._colHead34 = None
+ self._opaHead34 = None
+ self._colMetaTx = None
+ self._opaMetaTx = None
+
+ return
+
+ ##
+ # Setters
+ ##
+
+ def setLanguage(self, theLang):
+ """Set language for the document.
+ """
+ if theLang is None:
+ return False
+
+ langBits = theLang.split("_")
+ self._dLanguage = langBits[0]
+ if len(langBits) > 1:
+ self._dCountry = langBits[1]
+
+ return True
+
+ def setColourHeaders(self, doColour):
+ """Enable/disable coloured headings and comments.
+ """
+ self.colourHead = doColour
+ return
+
+ ##
+ # Class Methods
+ ##
+
+ def initDocument(self):
+ """Initialises a new open document XML tree.
+ """
+ # Initialise Variables
+ # ====================
+
+ self._fontFamily = self.textFont
+ if len(self.textFont.split()) > 1:
+ self._fontFamily = f"'{self.textFont}'"
+ self._fontPitch = "fixed" if self.textFixed else "variable"
+
+ self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt"
+ self._fSizeHead1 = f"{round(2.00 * self.textSize):d}pt"
+ self._fSizeHead2 = f"{round(1.60 * self.textSize):d}pt"
+ self._fSizeHead3 = f"{round(1.30 * self.textSize):d}pt"
+ self._fSizeHead4 = f"{round(1.15 * self.textSize):d}pt"
+ self._fSizeHead = f"{round(1.15 * self.textSize):d}pt"
+ self._fSizeText = f"{self.textSize:d}pt"
+
+ self._mTopTitle = self._emToCm(self.marginTitle[0])
+ self._mTopHead1 = self._emToCm(self.marginHead1[0])
+ self._mTopHead2 = self._emToCm(self.marginHead2[0])
+ self._mTopHead3 = self._emToCm(self.marginHead3[0])
+ self._mTopHead4 = self._emToCm(self.marginHead4[0])
+ self._mTopHead = self._emToCm(self.marginHead4[0])
+ self._mTopText = self._emToCm(self.marginText[0])
+ self._mTopMeta = self._emToCm(self.marginMeta[0])
+
+ self._mBotTitle = self._emToCm(self.marginTitle[1])
+ self._mBotHead1 = self._emToCm(self.marginHead1[1])
+ self._mBotHead2 = self._emToCm(self.marginHead2[1])
+ self._mBotHead3 = self._emToCm(self.marginHead3[1])
+ self._mBotHead4 = self._emToCm(self.marginHead4[1])
+ self._mBotHead = self._emToCm(self.marginHead4[1])
+ self._mBotText = self._emToCm(self.marginText[1])
+ self._mBotMeta = self._emToCm(self.marginMeta[1])
+
+ if self.colourHead:
+ self._colHead12 = "#2a6099"
+ self._opaHead12 = "100%"
+ self._colHead34 = "#444444"
+ self._opaHead34 = "100%"
+ self._colMetaTx = "#813709"
+ self._opaMetaTx = "100%"
+
+ self._lineHeight = f"{round(100 * self.lineHeight):d}%"
+ self._textAlign = "justify" if self.doJustify else "left"
+
+ # Create Roots
+ # ============
+
+ tAttr = {}
+ tAttr[_mkTag("office", "version")] = X_VERS
+
+ fAttr = {}
+ fAttr[_mkTag("style", "name")] = self.textFont
+ fAttr[_mkTag("style", "font-pitch")] = self._fontPitch
+
+ if self._isFlat:
+
+ # FODT File
+ # =========
+
+ tAttr[_mkTag("office", "mimetype")] = X_MIME
+
+ tFlat = _mkTag("office", "document")
+ self._dFlat = etree.Element(tFlat, attrib=tAttr, nsmap=XML_NS)
+
+ self._xMeta = etree.SubElement(self._dFlat, _mkTag("office", "meta"))
+ self._xFont = etree.SubElement(self._dFlat, _mkTag("office", "font-face-decls"))
+ self._xStyl = etree.SubElement(self._dFlat, _mkTag("office", "styles"))
+ self._xAuto = etree.SubElement(self._dFlat, _mkTag("office", "automatic-styles"))
+ self._xBody = etree.SubElement(self._dFlat, _mkTag("office", "body"))
+
+ etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr)
+
+ else:
+
+ # ODT File
+ # ========
+
+ tCont = _mkTag("office", "document-content")
+ tMeta = _mkTag("office", "document-meta")
+ tStyl = _mkTag("office", "document-styles")
+
+ self._dCont = etree.Element(tCont, attrib=tAttr, nsmap=XML_NS)
+ self._xFnt1 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls"))
+ self._xAuto = etree.SubElement(self._dCont, _mkTag("office", "automatic-styles"))
+ self._xBody = etree.SubElement(self._dCont, _mkTag("office", "body"))
+
+ self._dMeta = etree.Element(tMeta, attrib=tAttr, nsmap=XML_NS)
+ self._xMeta = etree.SubElement(self._dMeta, _mkTag("office", "meta"))
+
+ self._dStyl = etree.Element(tStyl, attrib=tAttr, nsmap=XML_NS)
+ self._xFnt2 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls"))
+ self._xStyl = etree.SubElement(self._dStyl, _mkTag("office", "styles"))
+
+ etree.SubElement(self._xFnt1, _mkTag("style", "font-face"), attrib=fAttr)
+ etree.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr)
+
+ # Finalise
+ # ========
+
+ self._xText = etree.SubElement(self._xBody, _mkTag("office", "text"))
+
+ # Meta Data
+ xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
+ xMeta.text = datetime.now().isoformat()
+
+ xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
+ xMeta.text = f"novelWriter/{nw.__version__}"
+
+ self._defaultStyles()
+ self._useableStyles()
+
+ return
+
+ def doConvert(self):
+ """Convert the list of text tokens into XML elements.
+ """
+ self.theResult = "" # Not used, but cleared just in case
+
+ odtTags = {
+ self.FMT_B_B : "_B", # Bold open format
+ self.FMT_B_E : "b_", # Bold close format
+ self.FMT_I_B : "I", # Italic open format
+ self.FMT_I_E : "i", # Italic close format
+ self.FMT_D_B : "_S", # Strikethrough open format
+ self.FMT_D_E : "s_", # Strikethrough close format
+ }
+
+ thisPar = []
+ thisFmt = []
+ parStyle = None
+ hasHardBreak = False
+ for tType, tLine, tText, tFormat, tStyle in self.theTokens:
+
+ # Styles
+ oStyle = ODTParagraphStyle()
+ if tStyle is not None:
+ if tStyle & self.A_LEFT:
+ oStyle.setTextAlign("left")
+ elif tStyle & self.A_RIGHT:
+ oStyle.setTextAlign("right")
+ elif tStyle & self.A_CENTRE:
+ oStyle.setTextAlign("center")
+ elif tStyle & self.A_JUSTIFY:
+ oStyle.setTextAlign("justify")
+
+ if tStyle & self.A_PBB:
+ oStyle.setBreakBefore("page")
+ elif tStyle & self.A_PBB_AUT:
+ oStyle.setBreakBefore("auto")
+
+ if tStyle & self.A_PBA:
+ oStyle.setBreakAfter("page")
+ elif tStyle & self.A_PBA_AUT:
+ oStyle.setBreakAfter("auto")
+
+ if tStyle & self.A_Z_BTMMRG:
+ oStyle.setMarginBottom("0.000cm")
+ if tStyle & self.A_Z_TOPMRG:
+ oStyle.setMarginTop("0.000cm")
+
+ # Process Text Types
+ if tType == self.T_EMPTY:
+ if hasHardBreak and parStyle is not None:
+ if self.doJustify:
+ parStyle.setTextAlign("left")
+
+ if len(thisPar) > 0:
+ tTemp = "".join(thisPar)
+ fTemp = "".join(thisFmt)
+ tTxt = tTemp.rstrip()
+ tFmt = fTemp[:len(tTxt)]
+ self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
+
+ thisPar = []
+ thisFmt = []
+ parStyle = None
+ hasHardBreak = False
+
+ elif tType == self.T_TITLE:
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Title", oStyle, tHead, isHead=True)
+
+ elif tType == self.T_HEAD1:
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_1", oStyle, tHead, isHead=True, oLevel="1")
+
+ elif tType == self.T_HEAD2:
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2")
+
+ elif tType == self.T_HEAD3:
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_3", oStyle, tHead, isHead=True, oLevel="3")
+
+ elif tType == self.T_HEAD4:
+ tHead = tText.replace(r"\\", "\n")
+ self._addTextPar("Heading_4", oStyle, tHead, isHead=True, oLevel="4")
+
+ elif tType == self.T_SEP:
+ self._addTextPar("Text_Body", oStyle, tText)
+
+ elif tType == self.T_SKIP:
+ self._addTextPar("Text_Body", oStyle, "")
+
+ elif tType == self.T_TEXT:
+ tTemp = tText
+ if parStyle is None:
+ parStyle = oStyle
+
+ tFmt = " "*len(tTemp)
+ for xPos, xLen, xFmt in tFormat:
+ tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
+
+ tTxt = tTemp.rstrip()
+ tFmt = tFmt[:len(tTxt)]
+ if tText.endswith(" "):
+ thisPar.append(tTxt + "\n")
+ thisFmt.append(tFmt + " ")
+ hasHardBreak = True
+ else:
+ thisPar.append(tTxt + " ")
+ thisFmt.append(tFmt + " ")
+
+ elif tType == self.T_SYNOPSIS and self.doSynopsis:
+ tTemp, fTemp = self._formatSynopsis(tText)
+ self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
+
+ elif tType == self.T_COMMENT and self.doComments:
+ tTemp, fTemp = self._formatComments(tText)
+ self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
+
+ elif tType == self.T_KEYWORD and self.doKeywords:
+ tTemp, fTemp = self._formatKeywords(tText)
+ self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
+
+ return
+
+ def closeDocument(self):
+ """Return the serialised XML document
+ """
+ # Build the auto-generated styles
+ for styleName, styleObj in self._autoPara.values():
+ styleObj.packXML(self._xAuto, styleName)
+ for styleName, styleObj in self._autoText.values():
+ styleObj.packXML(self._xAuto, styleName)
+
+ return
+
+ def saveFlatXML(self, savePath):
+ """Save the data to an .fodt file.
+ """
+ with open(savePath, mode="wb") as outFile:
+ outFile.write(etree.tostring(
+ self._dFlat,
+ pretty_print = True,
+ encoding = "utf-8",
+ xml_declaration = True
+ ))
+ return
+
+ def saveOpenDocText(self, savePath):
+ """Save the data to an .odt file.
+ """
+ mMap = {"manifest" : "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
+ mMani = "{%s}manifest" % mMap["manifest"]
+ mVers = "{%s}version" % mMap["manifest"]
+ mPath = "{%s}full-path" % mMap["manifest"]
+ mType = "{%s}media-type" % mMap["manifest"]
+ mFile = "{%s}file-entry" % mMap["manifest"]
+
+ xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=mMap)
+ etree.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
+ etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
+ etree.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
+ etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
+ etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
+
+ sMap = {"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
+ oRoot = "{%s}document-settings" % sMap["office"]
+ oSett = "{%s}settings" % sMap["office"]
+ xSett = etree.Element(oRoot, nsmap=sMap)
+ etree.SubElement(xSett, oSett)
+
+ with ZipFile(savePath, mode="w") as outFile:
+ outFile.writestr("mimetype", X_MIME)
+ outFile.writestr("META-INF/manifest.xml", etree.tostring(
+ xMani, pretty_print=False, encoding="utf-8", xml_declaration=True
+ ))
+ outFile.writestr("settings.xml", etree.tostring(
+ xSett, pretty_print=False, encoding="utf-8", xml_declaration=True
+ ))
+ outFile.writestr("content.xml", etree.tostring(
+ self._dCont, pretty_print=False, encoding="utf-8", xml_declaration=True
+ ))
+ outFile.writestr("meta.xml", etree.tostring(
+ self._dMeta, pretty_print=False, encoding="utf-8", xml_declaration=True
+ ))
+ outFile.writestr("styles.xml", etree.tostring(
+ self._dStyl, pretty_print=False, encoding="utf-8", xml_declaration=True
+ ))
+
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _formatSynopsis(self, tText):
+ """Apply formatting to synopsis lines.
+ """
+ rTxt = "**Synopsis:** %s" % tText
+ rFmt = "_B b_ %s" % (" "*len(tText))
+ return rTxt, rFmt
+
+ def _formatComments(self, tText):
+ """Apply formatting to comments.
+ """
+ rTxt = "**Comment:** %s" % tText
+ rFmt = "_B b_ %s" % (" "*len(tText))
+ return rTxt, rFmt
+
+ def _formatKeywords(self, tText):
+ """Apply formatting to keywords.
+ """
+ isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
+ if not isValid or not theBits:
+ return ""
+
+ rTxt = ""
+ rFmt = ""
+ if theBits[0] in nwLabels.KEY_NAME:
+ tText = nwLabels.KEY_NAME[theBits[0]]
+ rTxt += "**%s:** " % tText
+ rFmt += "_B%s b_ " % (" "*len(tText))
+ if len(theBits) > 1:
+ if theBits[0] == nwKeyWords.TAG_KEY:
+ rTxt += "%s" % theBits[1]
+ rFmt += "%s" % (" "*len(theBits[1]))
+ else:
+ tTags = ", ".join(theBits[1:])
+ rTxt += tTags
+ rFmt += (" "*len(tTags))
+
+ return rTxt, rFmt
+
+ def _addTextPar(self, styleName, oStyle, theText, theFmt="", isHead=False, oLevel=None):
+ """Add a text paragraph to the text XML element.
+ """
+ tAttr = {}
+ tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle)
+ if oLevel is not None:
+ tAttr[_mkTag("text", "outline-level")] = oLevel
+
+ pTag = "h" if isHead else "p"
+ xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
+
+ if not theText:
+ return
+
+ ##
+ # Process Formatting
+ ##
+
+ if len(theText) != len(theFmt):
+ # Genrate dummy format if there isn't any
+ theFmt = " "*len(theText)
+
+ # XML functions
+ xTail = None
+
+ def appendText(tText):
+ nonlocal xElem, xTail
+ if tText:
+ if xTail is None:
+ xElem.text = tText
+ else:
+ xTail.tail = tText
+
+ def appendSpan(tText, tFmt):
+ nonlocal xElem, xTail
+ if tText:
+ xTail = etree.SubElement(xElem, TAG_SPAN, attrib={
+ TAG_STNM: self._textStyle(tFmt)
+ })
+ xTail.text = tText
+
+ # The formatting loop
+ tTemp = ""
+ xFmt = 0x00
+ pFmt = 0x00
+
+ for i, c in enumerate(theText):
+
+ if theFmt[i] == "_":
+ continue
+ elif theFmt[i] == "B":
+ xFmt |= self.X_BLD
+ elif theFmt[i] == "b":
+ xFmt ^= self.X_BLD
+ elif theFmt[i] == "I":
+ xFmt |= self.X_ITA
+ elif theFmt[i] == "i":
+ xFmt ^= self.X_ITA
+ elif theFmt[i] == "S":
+ xFmt |= self.X_DEL
+ elif theFmt[i] == "s":
+ xFmt ^= self.X_DEL
+
+ if c == "\n":
+ xFmt |= self.X_BRK
+ c = ""
+ elif c == "\t":
+ xFmt |= self.X_TAB
+ c = ""
+
+ if theFmt[i] == " ":
+ tTemp += c
+
+ if xFmt != pFmt:
+ if pFmt == 0x00:
+ appendText(tTemp)
+ tTemp = ""
+ else:
+ appendSpan(tTemp, pFmt)
+ tTemp = ""
+
+ if xFmt & self.X_BRK:
+ xTail = etree.SubElement(xElem, TAG_BR)
+ xFmt ^= self.X_BRK
+
+ if xFmt & self.X_TAB:
+ xTail = etree.SubElement(xElem, TAG_TAB)
+ xFmt ^= self.X_TAB
+
+ pFmt = xFmt
+
+ # Save what remains in the buffer
+ appendText(tTemp)
+
+ return
+
+ def _paraStyle(self, parName, oStyle):
+ """Return a name for a style object.
+ """
+ refStyle = self._mainPara.get(parName, None)
+ if refStyle is None:
+ logger.error("Unknown paragraph style '%s'" % parName)
+ return "Standard"
+
+ if not refStyle.checkNew(oStyle):
+ return parName
+
+ oStyle.setParentStyleName(parName)
+ theID = oStyle.getID()
+ if theID in self._autoPara:
+ return self._autoPara[theID][0]
+
+ newName = "P%d" % (len(self._autoPara) + 1)
+ self._autoPara[theID] = (newName, oStyle)
+
+ return newName
+
+ def _textStyle(self, styleCode):
+ """Return a text style for a given style code.
+ """
+ if styleCode in self._autoText:
+ return self._autoText[styleCode][0]
+
+ newName = "T%d" % (len(self._autoText) + 1)
+ newStyle = ODTTextStyle()
+ if styleCode & self.X_BLD:
+ newStyle.setFontWeight("bold")
+ if styleCode & self.X_ITA:
+ newStyle.setFontStyle("italic")
+ if styleCode & self.X_DEL:
+ newStyle.setStrikeStyle("solid")
+ newStyle.setStrikeType("single")
+
+ self._autoText[styleCode] = (newName, newStyle)
+
+ return newName
+
+ def _emToCm(self, emVal):
+ """Converts an em value to centimetres.
+ """
+ return f"{emVal*2.54/72*self.textSize:.3f}cm"
+
+ ##
+ # Style Elements
+ ##
+
+ def _defaultStyles(self):
+ """Set the default styles.
+ """
+ # Add Paragraph Family Style
+ # ==========================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "line-break")] = "strict"
+ theAttr[_mkTag("style", "tab-stop-distance")] = "1.251cm"
+ theAttr[_mkTag("style", "writing-mode")] = "page"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "font-name")] = self.textFont
+ theAttr[_mkTag("fo", "font-family")] = self._fontFamily
+ theAttr[_mkTag("fo", "font-size")] = self._fSizeText
+ theAttr[_mkTag("fo", "language")] = self._dLanguage
+ theAttr[_mkTag("fo", "country")] = self._dCountry
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Standard Paragraph Style
+ # ============================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Standard"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "font-name")] = self.textFont
+ theAttr[_mkTag("fo", "font-family")] = self._fontFamily
+ theAttr[_mkTag("fo", "font-size")] = self._fSizeText
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ # Add Default Heading Style
+ # =========================
+
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = "Heading"
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ theAttr[_mkTag("style", "parent-style-name")] = "Standard"
+ theAttr[_mkTag("style", "next-style-name")] = "Text_Body"
+ theAttr[_mkTag("style", "class")] = "text"
+ xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("fo", "margin-top")] = self._mTopHead
+ theAttr[_mkTag("fo", "margin-bottom")] = self._mBotHead
+ theAttr[_mkTag("fo", "keep-with-next")] = "always"
+ etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ theAttr[_mkTag("style", "font-name")] = self.textFont
+ theAttr[_mkTag("fo", "font-family")] = self._fontFamily
+ theAttr[_mkTag("fo", "font-size")] = self._fSizeHead
+ etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ return
+
+ def _useableStyles(self):
+ """Set the usable styles.
+ """
+ # Add Text Body Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Text Body")
+ oStyle.setParentStyleName("Standard")
+ oStyle.setClass("text")
+ oStyle.setMarginTop(self._mTopText)
+ oStyle.setMarginBottom(self._mBotText)
+ oStyle.setLineHeight(self._lineHeight)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeText)
+ oStyle.setTextAlign(self._textAlign)
+ oStyle.packXML(self._xStyl, "Text_Body")
+
+ self._mainPara["Text_Body"] = oStyle
+
+ # Add Text Meta Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Text Meta")
+ oStyle.setParentStyleName("Standard")
+ oStyle.setClass("text")
+ oStyle.setMarginTop(self._mTopMeta)
+ oStyle.setMarginBottom(self._mBotMeta)
+ oStyle.setLineHeight(self._lineHeight)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeText)
+ oStyle.setColor(self._colMetaTx)
+ oStyle.setOpacity(self._opaMetaTx)
+ oStyle.packXML(self._xStyl, "Text_Meta")
+
+ self._mainPara["Text_Meta"] = oStyle
+
+ # Add Title Style
+ # ===============
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Title")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setClass("chapter")
+ oStyle.setTextAlign("center")
+ oStyle.setMarginTop(self._mTopTitle)
+ oStyle.setMarginBottom(self._mBotTitle)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeTitle)
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Title")
+
+ self._mainPara["Title"] = oStyle
+
+ # Add Heading 1 Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 1")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("1")
+ oStyle.setClass("text")
+ oStyle.setMarginTop(self._mTopHead1)
+ oStyle.setMarginBottom(self._mBotHead1)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead1)
+ oStyle.setColor(self._colHead12)
+ oStyle.setOpacity(self._opaHead12)
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_1")
+
+ self._mainPara["Heading_1"] = oStyle
+
+ # Add Heading 2 Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 2")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("2")
+ oStyle.setClass("text")
+ oStyle.setMarginTop(self._mTopHead2)
+ oStyle.setMarginBottom(self._mBotHead2)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead2)
+ oStyle.setColor(self._colHead12)
+ oStyle.setOpacity(self._opaHead12)
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_2")
+
+ self._mainPara["Heading_2"] = oStyle
+
+ # Add Heading 3 Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 3")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("3")
+ oStyle.setClass("text")
+ oStyle.setMarginTop(self._mTopHead3)
+ oStyle.setMarginBottom(self._mBotHead3)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead3)
+ oStyle.setColor(self._colHead34)
+ oStyle.setOpacity(self._opaHead34)
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_3")
+
+ self._mainPara["Heading_3"] = oStyle
+
+ # Add Heading 4 Style
+ # ===================
+
+ oStyle = ODTParagraphStyle()
+ oStyle.setDisplayName("Heading 4")
+ oStyle.setParentStyleName("Heading")
+ oStyle.setNextStyleName("Text_Body")
+ oStyle.setOutlineLevel("4")
+ oStyle.setClass("text")
+ oStyle.setMarginTop(self._mTopHead4)
+ oStyle.setMarginBottom(self._mBotHead4)
+ oStyle.setFontName(self.textFont)
+ oStyle.setFontFamily(self._fontFamily)
+ oStyle.setFontSize(self._fSizeHead4)
+ oStyle.setColor(self._colHead34)
+ oStyle.setOpacity(self._opaHead34)
+ oStyle.setFontWeight("bold")
+ oStyle.packXML(self._xStyl, "Heading_4")
+
+ self._mainPara["Heading_4"] = oStyle
+
+ return
+
+# END Class ToOdt
+
+# =============================================================================================== #
+# Auto-Style Classes
+# =============================================================================================== #
+
+class ODTParagraphStyle():
+ """Wrapper class for the paragraph style setting used by the
+ exporter. Only the used settings are exposed here to keep the class
+ minimal and fast.
+ """
+ VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
+ VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
+ VALID_LEVEL = ["1", "2", "3", "4"]
+ VALID_CLASS = ["text", "chapter"]
+ VALID_WEIGHT = ["normal", "inherit", "bold"]
+
+ def __init__(self):
+
+ # Attributes
+ self._mAttr = {
+ "display-name": ["style", None],
+ "parent-style-name": ["style", None],
+ "next-style-name": ["style", None],
+ "default-outline-level": ["style", None],
+ "class": ["style", None],
+ }
+
+ # Paragraph Attributes
+ self._pAttr = {
+ "margin-top": ["fo", None],
+ "margin-bottom": ["fo", None],
+ "line-height": ["fo", None],
+ "text-align": ["fo", None],
+ "break-before": ["fo", None],
+ "break-after": ["fo", None],
+ }
+
+ # Text Attributes
+ self._tAttr = {
+ "font-name": ["style", None],
+ "font-family": ["fo", None],
+ "font-size": ["fo", None],
+ "font-weight": ["fo", None],
+ "color": ["fo", None],
+ "opacity": ["loext", None],
+ }
+
+ return
+
+ ##
+ # Attribute Setters
+ ##
+
+ def setDisplayName(self, theValue):
+ self._mAttr["display-name"][1] = str(theValue)
+ return
+
+ def setParentStyleName(self, theValue):
+ self._mAttr["parent-style-name"][1] = str(theValue)
+ return
+
+ def setNextStyleName(self, theValue):
+ self._mAttr["next-style-name"][1] = str(theValue)
+ return
+
+ def setOutlineLevel(self, theValue):
+ if theValue in self.VALID_LEVEL:
+ self._mAttr["default-outline-level"][1] = str(theValue)
+ return
+
+ def setClass(self, theValue):
+ if theValue in self.VALID_CLASS:
+ self._mAttr["class"][1] = str(theValue)
+ return
+
+ ##
+ # Paragraph Setters
+ ##
+
+ def setMarginTop(self, theValue):
+ self._pAttr["margin-top"][1] = str(theValue)
+ return
+
+ def setMarginBottom(self, theValue):
+ self._pAttr["margin-bottom"][1] = str(theValue)
+ return
+
+ def setLineHeight(self, theValue):
+ self._pAttr["line-height"][1] = str(theValue)
+ return
+
+ def setTextAlign(self, theValue):
+ if theValue in self.VALID_ALIGN:
+ self._pAttr["text-align"][1] = str(theValue)
+ return
+
+ def setBreakBefore(self, theValue):
+ if theValue in self.VALID_BREAK:
+ self._pAttr["break-before"][1] = str(theValue)
+ return
+
+ def setBreakAfter(self, theValue):
+ if theValue in self.VALID_BREAK:
+ self._pAttr["break-after"][1] = str(theValue)
+ return
+
+ ##
+ # Text Setters
+ ##
+
+ def setFontName(self, theValue):
+ self._tAttr["font-name"][1] = str(theValue)
+ return
+
+ def setFontFamily(self, theValue):
+ self._tAttr["font-family"][1] = str(theValue)
+ return
+
+ def setFontSize(self, theValue):
+ self._tAttr["font-size"][1] = str(theValue)
+ return
+
+ def setFontWeight(self, theValue):
+ if theValue in self.VALID_WEIGHT:
+ self._tAttr["font-weight"][1] = str(theValue)
+ return
+
+ def setColor(self, theValue):
+ self._tAttr["color"][1] = str(theValue)
+ return
+
+ def setOpacity(self, theValue):
+ self._tAttr["opacity"][1] = str(theValue)
+ return
+
+ ##
+ # Getters
+ ##
+
+ def getAttr(self, attrName):
+ """Look through the dictionaries for the value, and return it if
+ we can find it, If not, return None.
+ """
+ retVal = self._mAttr.get(attrName, None)
+ if retVal is not None:
+ return retVal
+
+ retVal = self._pAttr.get(attrName, None)
+ if retVal is not None:
+ return retVal
+
+ retVal = self._tAttr.get(attrName, None)
+ if retVal is not None:
+ return retVal
+
+ return None
+
+ ##
+ # Methods
+ ##
+
+ def checkNew(self, refStyle):
+ """Check if there are new settings in refStyle that differ from
+ those in the current object.
+ """
+ for aName, (aNm, aVal) in refStyle._mAttr.items():
+ if aVal is not None and aVal != self._mAttr[aName][1]:
+ return True
+ for aName, (aNm, aVal) in refStyle._pAttr.items():
+ if aVal is not None and aVal != self._pAttr[aName][1]:
+ return True
+ for aName, (aNm, aVal) in refStyle._tAttr.items():
+ if aVal is not None and aVal != self._tAttr[aName][1]:
+ return True
+ return False
+
+ def getID(self):
+ """Generate a unique ID from the settings.
+ """
+ theString = (
+ f"Paragraph:Main:{str(self._mAttr)}:"
+ f"Paragraph:Para:{str(self._pAttr)}:"
+ f"Paragraph:Text:{str(self._tAttr)}:"
+ )
+ return sha256(theString.encode()).hexdigest()
+
+ def packXML(self, xParent, xName):
+ """Pack the content into an xml element.
+ """
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = xName
+ theAttr[_mkTag("style", "family")] = "paragraph"
+ for aName, (aNm, aVal) in self._mAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ for aName, (aNm, aVal) in self._pAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ if theAttr:
+ etree.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+
+ theAttr = {}
+ for aName, (aNm, aVal) in self._tAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ if theAttr:
+ etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ return
+
+# END Class ODTParagraphStyle
+
+class ODTTextStyle():
+ """Wrapper class for the text style setting used by the exporter.
+ Only the used settings are exposed here to keep the class minimal
+ and fast.
+ """
+ VALID_WEIGHT = ["normal", "inherit", "bold"]
+ VALID_STYLE = ["normal", "inherit", "italic"]
+ VALID_LSTYLE = ["none", "solid"]
+ VALID_LTYPE = ["none", "single", "double"]
+
+ def __init__(self):
+
+ # Text Attributes
+ self._tAttr = {
+ "font-weight": ["fo", None],
+ "font-style": ["fo", None],
+ "text-line-through-style": ["style", None],
+ "text-line-through-type": ["style", None],
+ }
+
+ return
+
+ ##
+ # Setters
+ ##
+
+ def setFontWeight(self, theValue):
+ if theValue in self.VALID_WEIGHT:
+ self._tAttr["font-weight"][1] = str(theValue)
+ return
+
+ def setFontStyle(self, theValue):
+ if theValue in self.VALID_STYLE:
+ self._tAttr["font-style"][1] = str(theValue)
+ return
+
+ def setStrikeStyle(self, theValue):
+ if theValue in self.VALID_LSTYLE:
+ self._tAttr["text-line-through-style"][1] = str(theValue)
+ return
+
+ def setStrikeType(self, theValue):
+ if theValue in self.VALID_LTYPE:
+ self._tAttr["text-line-through-type"][1] = str(theValue)
+ return
+
+ ##
+ # Methods
+ ##
+
+ def packXML(self, xParent, xName):
+ """Pack the content into an xml element.
+ """
+ theAttr = {}
+ theAttr[_mkTag("style", "name")] = xName
+ theAttr[_mkTag("style", "family")] = "text"
+ xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+
+ theAttr = {}
+ for aName, (aNm, aVal) in self._tAttr.items():
+ if aVal is not None:
+ theAttr[_mkTag(aNm, aName)] = aVal
+
+ if theAttr:
+ etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+
+ return
+
+# END Class ODTTextStyle
+
+# =============================================================================================== #
+# Local Functions
+# =============================================================================================== #
+
+def _mkTag(nsName, tagName):
+ """Assemble namespace and tag name.
+ """
+ theNS = XML_NS.get(nsName, "")
+ if theNS:
+ return "{%s}%s" % (theNS, tagName)
+ logger.warning("Missing xml namespace '%s'" % nsName)
+ return tagName
diff --git a/nw/gui/build.py b/nw/gui/build.py
index c24edb3e..d05aa0cc 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -35,7 +35,7 @@ from datetime import datetime
from PyQt5.QtCore import Qt, QByteArray, QTimer
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
- QPalette, QColor, QTextDocumentWriter, QFont, QCursor
+ QPalette, QColor, QTextDocumentWriter, QFont, QCursor, QFontInfo
)
from PyQt5.QtWidgets import (
qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
@@ -46,7 +46,7 @@ from PyQt5.QtWidgets import (
from nw.common import fuzzyTime, makeFileNameSafe
from nw.gui.custom import QSwitch
-from nw.core import ToHtml
+from nw.core import ToHtml, ToOdt
from nw.constants import (
nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
)
@@ -56,13 +56,14 @@ logger = logging.getLogger(__name__)
class GuiBuildNovel(QDialog):
FMT_ODT = 1
- FMT_PDF = 2
- FMT_HTM = 3
- FMT_MD = 4
- FMT_NWD = 5
- FMT_TXT = 6
- FMT_JSON_H = 7
- FMT_JSON_M = 8
+ FMT_FODT = 2
+ FMT_PDF = 3
+ FMT_HTM = 4
+ FMT_MD = 5
+ FMT_NWD = 6
+ FMT_TXT = 7
+ FMT_JSON_H = 8
+ FMT_JSON_M = 9
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
@@ -78,7 +79,7 @@ class GuiBuildNovel(QDialog):
self.htmlText = [] # List of html documents
self.htmlStyle = [] # List of html styles
- self.nwdText = [] # List of markdown documents
+ self.htmlSize = 0 # Size of the html document
self.buildTime = 0 # The timestamp of the last build
self.setWindowTitle("Build Novel Project")
@@ -386,6 +387,10 @@ class GuiBuildNovel(QDialog):
self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT))
self.saveMenu.addAction(self.saveODT)
+ self.saveFODT = QAction("Flat Open Document (.fodt)", self)
+ self.saveFODT.triggered.connect(lambda: self._saveDocument(self.FMT_FODT))
+ self.saveMenu.addAction(self.saveFODT)
+
self.savePDF = QAction("Portable Document Format (.pdf)", self)
self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.saveMenu.addAction(self.savePDF)
@@ -526,29 +531,80 @@ class GuiBuildNovel(QDialog):
else:
self.htmlText = []
self.htmlStyle = []
- self.nwdText = []
self.buildTime = 0
return False
return True
##
- # Slots
+ # Slots and Related
##
def _buildPreview(self):
"""Build a preview of the project in the document viewer.
"""
+ # Get Settings
+ justifyText = self.justifyText.isChecked()
+ noStyling = self.noStyling.isChecked()
+ textFont = self.textFont.text()
+ textSize = self.textSize.value()
+ replaceTabs = self.replaceTabs.isChecked()
+
+ self.htmlText = []
+ self.htmlStyle = []
+ self.htmlSize = 0
+
+ # Build Preview
+ # =============
+
+ makeHtml = ToHtml(self.theProject, self.theParent)
+ self._doBuild(makeHtml, isPreview=True)
+ if replaceTabs:
+ makeHtml.replaceTabs()
+
+ self.htmlText = makeHtml.fullHTML
+ self.htmlStyle = makeHtml.getStyleSheet()
+ self.htmlSize = makeHtml.getFullResultSize()
+ self.buildTime = int(time())
+
+ # Load Preview
+ # ============
+
+ self.docView.setTextFont(textFont, textSize)
+ self.docView.setJustify(justifyText)
+ if noStyling:
+ self.docView.clearStyleSheet()
+ else:
+ self.docView.setStyleSheet(self.htmlStyle)
+
+ if self.htmlSize < nwConst.MAX_BUILDSIZE:
+ self.docView.setContent(self.htmlText, self.buildTime)
+ self._enableQtSave(True)
+ else:
+ self.docView.setText(
+ "Failed to generate preview. The result is too big."
+ )
+ self._enableQtSave(False)
+
+ self._saveCache()
+
+ return
+
+ def _doBuild(self, bldObj, isPreview=False, doConvert=True):
+ """Rund the build with a specific build object.
+ """
+ tStart = int(time())
+
# Get Settings
fmtTitle = self.fmtTitle.text().strip()
fmtChapter = self.fmtChapter.text().strip()
fmtUnnumbered = self.fmtUnnumbered.text().strip()
fmtScene = self.fmtScene.text().strip()
fmtSection = self.fmtSection.text().strip()
- justifyText = self.justifyText.isChecked()
- noStyling = self.noStyling.isChecked()
textFont = self.textFont.text()
textSize = self.textSize.value()
+ justifyText = self.justifyText.isChecked()
+ noStyling = self.noStyling.isChecked()
incSynopsis = self.includeSynopsis.isChecked()
incComments = self.includeComments.isChecked()
incKeywords = self.includeKeywords.isChecked()
@@ -556,20 +612,34 @@ class GuiBuildNovel(QDialog):
noteFiles = self.noteFiles.isChecked()
ignoreFlag = self.ignoreFlag.isChecked()
includeBody = self.includeBody.isChecked()
- replaceTabs = self.replaceTabs.isChecked()
- makeHtml = ToHtml(self.theProject, self.theParent)
- makeHtml.setTitleFormat(fmtTitle)
- makeHtml.setChapterFormat(fmtChapter)
- makeHtml.setUnNumberedFormat(fmtUnnumbered)
- makeHtml.setSceneFormat(fmtScene, fmtScene == "")
- makeHtml.setSectionFormat(fmtSection, fmtSection == "")
- makeHtml.setBodyText(includeBody)
- makeHtml.setSynopsis(incSynopsis)
- makeHtml.setComments(incComments)
- makeHtml.setKeywords(incKeywords)
- makeHtml.setJustify(justifyText)
- makeHtml.setStyles(not noStyling)
+ # Get font information
+ fontInfo = QFontInfo(QFont(textFont, textSize))
+ textFixed = fontInfo.fixedPitch()
+
+ isHtml = isinstance(bldObj, ToHtml)
+ isOdt = isinstance(bldObj, ToOdt)
+
+ bldObj.setTitleFormat(fmtTitle)
+ bldObj.setChapterFormat(fmtChapter)
+ bldObj.setUnNumberedFormat(fmtUnnumbered)
+ bldObj.setSceneFormat(fmtScene, fmtScene == "")
+ bldObj.setSectionFormat(fmtSection, fmtSection == "")
+
+ bldObj.setFont(textFont, textSize, textFixed)
+ bldObj.setJustify(justifyText)
+
+ bldObj.setSynopsis(incSynopsis)
+ bldObj.setComments(incComments)
+ bldObj.setKeywords(incKeywords)
+ bldObj.setBodyText(includeBody)
+
+ if isHtml:
+ bldObj.setStyles(not noStyling)
+
+ if isOdt:
+ bldObj.setColourHeaders(not noStyling)
+ bldObj.initDocument()
# Make sure the tree order is correct
self.theParent.treeView.flushTreeOrder()
@@ -577,14 +647,6 @@ class GuiBuildNovel(QDialog):
self.buildProgress.setMaximum(len(self.theProject.projTree))
self.buildProgress.setValue(0)
- tStart = int(time())
-
- self.htmlText = []
- self.htmlStyle = []
- self.nwdText = []
-
- htmlSize = 0
-
for nItt, tItem in enumerate(self.theProject.projTree):
noteRoot = noteFiles
@@ -595,76 +657,44 @@ class GuiBuildNovel(QDialog):
try:
if noteRoot:
# Add headers for root folders of notes
- makeHtml.addRootHeading(tItem.itemHandle)
- makeHtml.doConvert()
- self.htmlText.append(makeHtml.getResult())
- self.nwdText.append(makeHtml.getFilteredMarkdown())
- htmlSize += makeHtml.getResultSize()
+ bldObj.addRootHeading(tItem.itemHandle)
+ if doConvert:
+ bldObj.doConvert()
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
- makeHtml.setText(tItem.itemHandle)
- makeHtml.doAutoReplace()
- makeHtml.tokenizeText()
- makeHtml.doHeaders()
- makeHtml.doConvert()
- makeHtml.doPostProcessing()
- self.htmlText.append(makeHtml.getResult())
- self.nwdText.append(makeHtml.getFilteredMarkdown())
- htmlSize += makeHtml.getResultSize()
+ bldObj.setText(tItem.itemHandle)
+ bldObj.doAutoReplace()
+ bldObj.tokenizeText()
+ bldObj.doHeaders()
+ if doConvert:
+ bldObj.doConvert()
+ bldObj.doPostProcessing()
except Exception as e:
logger.error("Failed to generate html of document '%s'" % tItem.itemHandle)
logger.error(str(e))
- self.docView.setText((
- "Failed to generate preview. "
- "Document with title '%s' could not be parsed."
- ) % tItem.itemName)
+ if isPreview:
+ self.docView.setText((
+ "Failed to generate preview. "
+ "Document with title '%s' could not be parsed."
+ ) % tItem.itemName)
+
return False
# Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1)
- if makeHtml.errData:
+ if isOdt:
+ bldObj.closeDocument()
+
+ tEnd = int(time())
+ logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart)))
+
+ if bldObj.errData:
self.theParent.makeAlert((
"There were problems when building the project:"
"
- %s"
- ) % "
- ".join(makeHtml.errData), nwAlert.ERROR)
-
- if replaceTabs:
- htmlText = []
- eightSpace = " "*8
- for aLine in self.htmlText:
- htmlText.append(aLine.replace("\t", eightSpace))
- self.htmlText = htmlText
-
- nwdText = []
- for aLine in self.nwdText:
- nwdText.append(aLine.replace("\t", " "))
- self.nwdText = nwdText
-
- tEnd = int(time())
- logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
- self.htmlStyle = makeHtml.getStyleSheet()
- self.buildTime = tEnd
-
- # Load the preview document with the html data
- self.docView.setTextFont(textFont, textSize)
- self.docView.setJustify(justifyText)
- if noStyling:
- self.docView.clearStyleSheet()
- else:
- self.docView.setStyleSheet(self.htmlStyle)
-
- if htmlSize < nwConst.MAX_BUILDSIZE:
- self.docView.setContent(self.htmlText, self.buildTime)
- self._enableQtSave(True)
- else:
- self.docView.setText(
- "Failed to generate preview. The result is too big."
- )
- self._enableQtSave(False)
-
- self._saveCache()
+ ) % "
- ".join(bldObj.errData), nwAlert.ERROR)
return
@@ -711,59 +741,59 @@ class GuiBuildNovel(QDialog):
def _saveDocument(self, theFormat):
"""Save the document to various formats.
"""
+ replaceTabs = self.replaceTabs.isChecked()
+
byteFmt = QByteArray()
fileExt = ""
textFmt = ""
- outTool = ""
- # Create the settings
+ # Settings
+ # ========
+
if theFormat == self.FMT_ODT:
- byteFmt.append("odf")
fileExt = "odt"
textFmt = "Open Document"
- outTool = "Qt"
+
+ elif theFormat == self.FMT_FODT:
+ fileExt = "fodt"
+ textFmt = "Flat Open Document"
elif theFormat == self.FMT_PDF:
fileExt = "pdf"
textFmt = "PDF"
- outTool = "QtPrint"
elif theFormat == self.FMT_HTM:
fileExt = "htm"
textFmt = "Plain HTML"
- outTool = "NW"
elif theFormat == self.FMT_MD:
byteFmt.append("markdown")
fileExt = "md"
textFmt = "Markdown"
- outTool = "Qt"
elif theFormat == self.FMT_NWD:
fileExt = "nwd"
textFmt = "%s Markdown" % nw.__package__
- outTool = "NW"
elif theFormat == self.FMT_TXT:
byteFmt.append("plaintext")
fileExt = "txt"
textFmt = "Plain Text"
- outTool = "Qt"
elif theFormat == self.FMT_JSON_H:
fileExt = "json"
textFmt = "JSON + %s HTML" % nw.__package__
- outTool = "NW"
elif theFormat == self.FMT_JSON_M:
fileExt = "json"
textFmt = "JSON + %s Markdown" % nw.__package__
- outTool = "NW"
else:
return False
- # Generate the file name
+ # Generate File Name
+ # ==================
+
if fileExt:
cleanName = makeFileNameSafe(self.theProject.projName)
@@ -786,87 +816,109 @@ class GuiBuildNovel(QDialog):
else:
return False
- # Do the actual writing
- wSuccess = False
+ # Build and Write
+ # ===============
+
errMsg = ""
- if outTool == "Qt":
+ wSuccess = False
+
+ if theFormat == self.FMT_MD or theFormat == self.FMT_TXT:
docWriter = QTextDocumentWriter()
docWriter.setFileName(savePath)
docWriter.setFormat(byteFmt)
wSuccess = docWriter.write(self.docView.qDocument)
- elif outTool == "NW":
+ elif theFormat == self.FMT_HTM:
+ makeHtml = ToHtml(self.theProject, self.theParent)
+ self._doBuild(makeHtml)
+ if replaceTabs:
+ makeHtml.replaceTabs()
+
try:
- with open(savePath, mode="w", encoding="utf8") as outFile:
- if theFormat == self.FMT_HTM:
- # Write novelWriter HTML data
- theStyle = self.htmlStyle.copy()
- theStyle.append(r"article {width: 800px; margin: 40px auto;}")
- bodyText = "".join(self.htmlText)
- bodyText = bodyText.replace("\t", " ")
-
- theHtml = (
- "\n"
- "\n"
- "\n"
- "\n"
- "{projTitle:s}\n"
- "\n"
- "\n"
- "\n"
- "\n"
- "{bodyText:s}\n"
- "\n"
- "\n"
- "\n"
- ).format(
- projTitle = self.theProject.projName,
- htmlStyle = "\n".join(theStyle),
- bodyText = bodyText,
- )
- outFile.write(theHtml)
-
- elif theFormat == self.FMT_NWD:
- # Write novelWriter markdown data
- for aLine in self.nwdText:
- outFile.write(aLine)
-
- elif theFormat == self.FMT_JSON_H or theFormat == self.FMT_JSON_M:
- jsonData = {
- "meta" : {
- "workingTitle" : self.theProject.projName,
- "novelTitle" : self.theProject.bookTitle,
- "authors" : self.theProject.bookAuthors,
- "buildTime" : self.buildTime,
- }
- }
-
- if theFormat == self.FMT_JSON_H:
- theBody = []
- for htmlPage in self.htmlText:
- theBody.append(htmlPage.rstrip("\n").split("\n"))
- jsonData["text"] = {
- "css" : self.htmlStyle,
- "html" : theBody,
- }
- elif theFormat == self.FMT_JSON_M:
- theBody = []
- for nwdPage in self.nwdText:
- theBody.append(nwdPage.split("\n"))
- jsonData["text"] = {
- "nwd" : theBody,
- }
-
- outFile.write(json.dumps(jsonData, indent=2))
-
+ makeHtml.saveHTML5(savePath)
wSuccess = True
-
except Exception as e:
errMsg = str(e)
- elif outTool == "QtPrint" and theFormat == self.FMT_PDF:
+ elif theFormat == self.FMT_NWD:
+ makeNwd = ToHtml(self.theProject, self.theParent)
+ makeNwd.setKeepMarkdown(True)
+ self._doBuild(makeNwd, doConvert=False)
+ if replaceTabs:
+ makeNwd.replaceTabs(spaceChar=" ")
+
+ try:
+ with open(savePath, mode="w", encoding="utf8") as outFile:
+ for nwdPage in makeNwd.theMarkdown:
+ outFile.write(nwdPage)
+ wSuccess = True
+ except Exception as e:
+ errMsg = str(e)
+
+ elif theFormat == self.FMT_FODT:
+ makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True)
+ self._doBuild(makeOdt)
+ try:
+ makeOdt.saveFlatXML(savePath)
+ wSuccess = True
+ except Exception as e:
+ errMsg = str(e)
+
+ elif theFormat == self.FMT_ODT:
+ makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False)
+ self._doBuild(makeOdt)
+ try:
+ makeOdt.saveOpenDocText(savePath)
+ wSuccess = True
+ except Exception as e:
+ errMsg = str(e)
+
+ elif theFormat == self.FMT_JSON_H or theFormat == self.FMT_JSON_M:
+ jsonData = {
+ "meta" : {
+ "workingTitle" : self.theProject.projName,
+ "novelTitle" : self.theProject.bookTitle,
+ "authors" : self.theProject.bookAuthors,
+ "buildTime" : self.buildTime,
+ }
+ }
+
+ if theFormat == self.FMT_JSON_H:
+ makeHtml = ToHtml(self.theProject, self.theParent)
+ self._doBuild(makeHtml)
+ if replaceTabs:
+ makeHtml.replaceTabs()
+
+ theBody = []
+ for htmlPage in makeHtml.fullHTML:
+ theBody.append(htmlPage.rstrip("\n").split("\n"))
+ jsonData["text"] = {
+ "css" : self.htmlStyle,
+ "html" : theBody,
+ }
+
+ elif theFormat == self.FMT_JSON_M:
+ makeNwd = ToHtml(self.theProject, self.theParent)
+ makeNwd.setKeepMarkdown(True)
+ self._doBuild(makeNwd, doConvert=False)
+ if replaceTabs:
+ makeNwd.replaceTabs(spaceChar=" ")
+
+ theBody = []
+ for nwdPage in makeNwd.theMarkdown:
+ theBody.append(nwdPage.split("\n"))
+ jsonData["text"] = {
+ "nwd" : theBody,
+ }
+
+ try:
+ with open(savePath, mode="w", encoding="utf8") as outFile:
+ outFile.write(json.dumps(jsonData, indent=2))
+ wSuccess = True
+ except Exception as e:
+ errMsg = str(e)
+
+ elif theFormat == self.FMT_PDF:
try:
thePrinter = QPrinter()
thePrinter.setOutputFormat(QPrinter.PdfFormat)
@@ -955,13 +1007,10 @@ class GuiBuildNovel(QDialog):
if "htmlStyle" in theData.keys():
self.htmlStyle = theData["htmlStyle"]
dataCount += 1
- if "nwdText" in theData.keys():
- self.nwdText = theData["nwdText"]
- dataCount += 1
if "buildTime" in theData.keys():
self.buildTime = theData["buildTime"]
- return dataCount == 3
+ return dataCount == 2
def _saveCache(self):
"""Save the current data to cache.
@@ -974,7 +1023,6 @@ class GuiBuildNovel(QDialog):
outFile.write(json.dumps({
"htmlText" : self.htmlText,
"htmlStyle" : self.htmlStyle,
- "nwdText" : self.nwdText,
"buildTime" : self.buildTime,
}, indent=2))
except Exception as e:
diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py
index 0fabaae9..d37cebee 100644
--- a/nw/gui/projdetails.py
+++ b/nw/gui/projdetails.py
@@ -5,7 +5,7 @@ novelWriter – GUI Project Details
Class holding the project details dialog
File History:
-Created: 2021-01-03 [1.0a0]
+Created: 2021-01-03 [1.1a0]
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd
index 6a1302ef..be611dce 100644
--- a/sample/content/636b6aa9b697b.nwd
+++ b/sample/content/636b6aa9b697b.nwd
@@ -9,7 +9,7 @@
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree.
-Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isn’t fully Markdown compliant. If the syntax highlighter doesn’t show it correctly, the export tool will not either.
+Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isn’t fully Markdown compliant. If the syntax highlighter doesn’t show it correctly, the export tool will not either.
In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
diff --git a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
index a3e4432f..413f48df 100644
--- a/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
+++ b/tests/reference/guiBuild_Tool_Step1_Lorem_Ipsum.htm
@@ -5,6 +5,7 @@
Lorem Ipsum
-Lorem Ipsum
+Lorem Ipsum
By lipsum.com
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
@@ -65,7 +66,6 @@ article {width: 800px; margin: 40px auto;}
Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis.
Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue.
Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.
-