Merge pull request #221 from vkbo/build_updates

Build Project Updates
This commit is contained in:
Veronica K. Berglyd Olsen
2020-05-23 21:21:35 +02:00
committed by GitHub
6 changed files with 293 additions and 168 deletions
+4
View File
@@ -2,6 +2,10 @@
## Version 0.6 [2020-xx-xx] ## Version 0.6 [2020-xx-xx]
**Bugfixes**
* Fixed a bug in validation of `@tag:` meta tags where one or more spaces before the `:` would still pass as valid tags, but the keyword index array would be missing those spaces in its counter. This mainly affected the highlighting of keywords, which would be misaligned. PR #206
**User Interface** **User Interface**
* The Export Tool has been removed and replaced by a new tool called "Build Novel Project". The new tool has the same filtering options as the Export Tool, but with more formatting options for titles. It also has a preview window to display the generated document. A Save As button provides exports to HTML, novelWriter Markdown. plain text, PDF and Open Document format. LaTeX export has not been ported over, and interfacing with Pandoc is no longer supported either. Although, as before, the HTML export can be converted with Pandoc to other formats outside of novelWriter. The new tool also supports printing. PR #204 * The Export Tool has been removed and replaced by a new tool called "Build Novel Project". The new tool has the same filtering options as the Export Tool, but with more formatting options for titles. It also has a preview window to display the generated document. A Save As button provides exports to HTML, novelWriter Markdown. plain text, PDF and Open Document format. LaTeX export has not been ported over, and interfacing with Pandoc is no longer supported either. Although, as before, the HTML export can be converted with Pandoc to other formats outside of novelWriter. The new tool also supports printing. PR #204
+95 -30
View File
@@ -48,12 +48,16 @@ class ToHtml(Tokenizer):
"<" : "&lt;", "<" : "&lt;",
">" : "&gt;", ">" : "&gt;",
"&" : "&amp;", "&" : "&amp;",
"\t" : "&emsp;", "\t" : "&emsp;"*2,
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH, nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP, nwUnicode.U_NBSP : nwUnicode.H_NBSP,
} }
self.revDict = {}
self.reReplace = []
self.reReverse = []
self._buildRegEx()
return return
@@ -71,6 +75,7 @@ class ToHtml(Tokenizer):
self.doKeywords = True self.doKeywords = True
self.doComments = doComments self.doComments = doComments
self.repDict["\t"] = "&nbsp;"*8 self.repDict["\t"] = "&nbsp;"*8
self._buildRegEx()
return return
## ##
@@ -82,10 +87,9 @@ class ToHtml(Tokenizer):
characters into their respective HTML entities. characters into their respective HTML entities.
""" """
Tokenizer.doAutoReplace(self) Tokenizer.doAutoReplace(self)
self.theText = self.reReplace.sub(
xRep = re.compile("|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL) lambda x: self.repDict[x.group(0)], self.theText
self.theText = xRep.sub(lambda x: self.repDict[x.group(0)], self.theText) )
return return
def doPostProcessing(self): def doPostProcessing(self):
@@ -95,18 +99,15 @@ class ToHtml(Tokenizer):
if self.genMode == self.M_PREVIEW: if self.genMode == self.M_PREVIEW:
# Doesn't matter for preview as we don't use the markdown # Doesn't matter for preview as we don't use the markdown
return return
self.theMarkdown = self.reReverse.sub(
revDict = dict(map(reversed, self.repDict.items())) lambda x: self.revDict[x.group(0)], self.theMarkdown
xRep = re.compile("|".join([re.escape(k) for k in revDict.keys()]), flags=re.DOTALL) )
self.theMarkdown = xRep.sub(lambda x: revDict[x.group(0)], self.theMarkdown)
return return
def doConvert(self): def doConvert(self):
"""Convert the list of text tokens into a HTML document saved """Convert the list of text tokens into a HTML document saved
to theResult. to theResult.
""" """
htmlTags = { htmlTags = {
self.FMT_B_B : "<strong>", self.FMT_B_B : "<strong>",
self.FMT_B_E : "</strong>", self.FMT_B_E : "</strong>",
@@ -116,11 +117,32 @@ class ToHtml(Tokenizer):
self.FMT_U_E : "</u>", self.FMT_U_E : "</u>",
} }
if self.isNovel and self.genMode != self.M_PREVIEW:
# For novel files for export, we bump the titles one level
# up as this is more useful for printing and word processor
# imports.
h1 = "h1 class='title'"
h2 = "h1"
h3 = "h2"
h4 = "h3"
else:
h1 = "h1"
h2 = "h2"
h3 = "h3"
h4 = "h4"
alignHead = self.A_LEFT
if self.doJustify:
alignPar = self.A_JUSTIFY
else:
alignPar = self.A_LEFT
self.theResult = "" self.theResult = ""
thisPar = [] thisPar = []
parStyle = "" parStyle = None
tmpResult = [] tmpResult = []
hasHardBreak = False
for tType, tText, tFormat, tStyle in self.theTokens: for tType, tText, tFormat, tStyle in self.theTokens:
# Styles # Styles
@@ -136,20 +158,16 @@ class ToHtml(Tokenizer):
aStyle.append("text-align: justify;") aStyle.append("text-align: justify;")
if tStyle & self.A_PBB: if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;") aStyle.append("page-break-before: always;")
if tStyle & self.A_PBB_L:
aStyle.append("page-break-before: left;")
if tStyle & self.A_PBB_R:
aStyle.append("page-break-before: right;")
if tStyle & self.A_PBB_AV: if tStyle & self.A_PBB_AV:
aStyle.append("page-break-before: avoid;") aStyle.append("page-break-before: avoid;")
if tStyle & self.A_PBB_NO:
aStyle.append("page-break-before: never;")
if tStyle & self.A_PBA: if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;") aStyle.append("page-break-after: always;")
if tStyle & self.A_PBA_L:
aStyle.append("page-break-after: left;")
if tStyle & self.A_PBA_R:
aStyle.append("page-break-after: right;")
if tStyle & self.A_PBA_AV: if tStyle & self.A_PBA_AV:
aStyle.append("page-break-after: avoid;") aStyle.append("page-break-after: avoid;")
if tStyle & self.A_PBA_NO:
aStyle.append("page-break-after: never;")
if len(aStyle) > 0: if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle)) hStyle = " style='%s'" % (" ".join(aStyle))
@@ -158,41 +176,54 @@ class ToHtml(Tokenizer):
# Process TextType # Process TextType
if tType == self.T_EMPTY: if tType == self.T_EMPTY:
if parStyle is None:
parStyle = ""
if hasHardBreak:
parClass = " class='break'"
else:
parClass = ""
if len(thisPar) > 0: if len(thisPar) > 0:
tTemp = "".join(thisPar) tTemp = "".join(thisPar)
tmpResult.append("<p%s>%s</p>\n" % (parStyle, tTemp.rstrip())) tmpResult.append("<p%s%s>%s</p>\n" % (parStyle, parClass, tTemp.rstrip()))
thisPar = [] thisPar = []
parStyle = "" parStyle = None
hasHardBreak = False
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1 class='title'%s>%s</h1>\n" % (hStyle, tHead))
elif tType == self.T_HEAD1: elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1%s>%s</h1>\n" % (hStyle, tHead)) tmpResult.append("<%s%s>%s</%s>\n" % (h1, hStyle, tHead, h1))
elif tType == self.T_HEAD2: elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h2%s>%s</h2>\n" % (hStyle, tHead)) tmpResult.append("<%s%s>%s</%s>\n" % (h2, hStyle, tHead, h2))
elif tType == self.T_HEAD3: elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h3%s>%s</h3>\n" % (hStyle, tHead)) tmpResult.append("<%s%s>%s</%s>\n" % (h3, hStyle, tHead, h3))
elif tType == self.T_HEAD4: elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>") tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h4%s>%s</h4>\n" % (hStyle, tHead)) tmpResult.append("<%s%s>%s</%s>\n" % (h4, hStyle, tHead, h4))
elif tType == self.T_SEP: elif tType == self.T_SEP:
tmpResult.append("<p%s>%s</p>\n" % (hStyle, tText)) tmpResult.append("<p class='sep'>%s</p>\n" % tText)
elif tType == self.T_SKIP: elif tType == self.T_SKIP:
tmpResult.append("<p%s>&nbsp;</p>\n" % hStyle) tmpResult.append("<p class='skip'>&nbsp;</p>\n")
elif tType == self.T_TEXT: elif tType == self.T_TEXT:
tTemp = tText tTemp = tText
parStyle = hStyle if parStyle is None:
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat): 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(" "): if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"<br/>") thisPar.append(tTemp.rstrip()+"<br/>")
hasHardBreak = True
else: else:
thisPar.append(tTemp.rstrip()+" ") thisPar.append(tTemp.rstrip()+" ")
@@ -210,6 +241,29 @@ class ToHtml(Tokenizer):
return return
def getStylesheet(self):
"""Generate a stylesheet appropriate for the current settings.
"""
theStyles = []
if self.doJustify:
theStyles.append(r"p {text-align: justify;}")
else:
theStyles.append(r"p {text-align: 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".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);}")
return theStyles
## ##
# Internal Functions # Internal Functions
## ##
@@ -233,7 +287,6 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText): def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords. """Apply HTML formatting to keywords.
""" """
tText = "@"+tText tText = "@"+tText
isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText) isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText)
if not isValid or not theBits: if not isValid or not theBits:
@@ -263,4 +316,16 @@ class ToHtml(Tokenizer):
return "<div>%s</div>" % retText return "<div>%s</div>" % retText
def _buildRegEx(self):
"""Build the regular expressions
"""
self.revDict = dict(map(reversed, self.repDict.items()))
self.reReplace = re.compile(
"|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL
)
self.reReverse = re.compile(
"|".join([re.escape(k) for k in self.revDict.keys()]), flags=re.DOTALL
)
return
# END Class ToHtml # END Class ToHtml
+74 -48
View File
@@ -34,7 +34,7 @@ from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc from nw.core.document import NWDoc
from nw.core.tools import numberToWord from nw.core.tools import numberToWord
from nw.constants import nwItemLayout from nw.constants import nwItemLayout, nwItemType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,26 +51,26 @@ class Tokenizer():
T_SYNOPSIS = 2 # Synopsis comment T_SYNOPSIS = 2 # Synopsis comment
T_COMMENT = 3 # Comment line T_COMMENT = 3 # Comment line
T_KEYWORD = 4 # Command line T_KEYWORD = 4 # Command line
T_HEAD1 = 5 # Header 1 (title) T_TITLE = 5 # Title
T_HEAD2 = 6 # Header 2 (chapter) T_HEAD1 = 6 # Header 1
T_HEAD3 = 7 # Header 3 (scene) T_HEAD2 = 7 # Header 2
T_HEAD4 = 8 # Header 4 T_HEAD3 = 8 # Header 3
T_TEXT = 9 # Text line T_HEAD4 = 9 # Header 4
T_SEP = 10 # Scene separator T_TEXT = 10 # Text line
T_SKIP = 11 # Paragraph break T_SEP = 11 # Scene separator
T_SKIP = 12 # Paragraph break
A_LEFT = 1 # Left aligned A_NONE = 0 # No special style
A_RIGHT = 2 # Right aligned A_LEFT = 1 # Left aligned
A_CENTRE = 4 # Centred A_RIGHT = 2 # Right aligned
A_JUSTIFY = 8 # Justified A_CENTRE = 4 # Centred
A_PBB = 16 # Page break before A_JUSTIFY = 8 # Justified
A_PBB_L = 32 # Page break before, left A_PBB = 16 # Page break before always
A_PBB_R = 64 # Page break before, right A_PBB_AV = 32 # Page break before avoid
A_PBB_AV = 128 # Page break, avoid A_PBB_NO = 64 # Page break before never
A_PBA = 256 # Page break after A_PBA = 128 # Page break after always
A_PBA_L = 512 # Page break after, left A_PBA_AV = 256 # Page break after avoid
A_PBA_R = 1024 # Page break after, right A_PBA_NO = 512 # Page break after avoid
A_PBA_AV = 2048 # Page break, avoid
def __init__(self, theProject, theParent): def __init__(self, theProject, theParent):
@@ -198,6 +198,25 @@ class Tokenizer():
# Class Methods # Class Methods
## ##
def addRootHeading(self, theHandle):
"""Add a heading at the start if a new root folder.
"""
theItem = self.theProject.projTree[theHandle]
if theItem is None:
return False
if theItem.itemType != nwItemType.ROOT:
return False
theTitle = "Notes: %s" % theItem.itemName
self.theTokens = []
self.theTokens.append((
self.T_TITLE, theTitle, None, self.A_PBB | self.A_CENTRE
))
self.theMarkdown = "# %s\n\n" % theTitle
return True
def setText(self, theHandle, theText=None): def setText(self, theHandle, theText=None):
"""Set the text for the tokenizer from a handle. If theText is """Set the text for the tokenizer from a handle. If theText is
not set, load it from the file. not set, load it from the file.
@@ -205,6 +224,8 @@ class Tokenizer():
self.theHandle = theHandle self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle] self.theItem = self.theProject.projTree[theHandle]
if self.theItem is None:
return
if theText is not None: if theText is not None:
# If the text is set, just use that # If the text is set, just use that
@@ -283,11 +304,6 @@ class Tokenizer():
[None, self.FMT_U_B, None, self.FMT_U_E] [None, self.FMT_U_B, None, self.FMT_U_E]
)] )]
if self.doJustify:
defAlign = self.A_JUSTIFY
else:
defAlign = self.A_LEFT
self.theTokens = [] self.theTokens = []
self.theMarkdown = "" self.theMarkdown = ""
tmpMarkdown = [] tmpMarkdown = []
@@ -296,7 +312,7 @@ class Tokenizer():
# Tag lines starting with specific characters # Tag lines starting with specific characters
if len(aLine.strip()) == 0: if len(aLine.strip()) == 0:
self.theTokens.append(( self.theTokens.append((
self.T_EMPTY, "", None, None self.T_EMPTY, "", None, self.A_NONE
)) ))
tmpMarkdown.append("\n") tmpMarkdown.append("\n")
@@ -304,45 +320,45 @@ class Tokenizer():
cLine = aLine[1:].strip() cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"): if cLine.lower().startswith("synopsis:"):
self.theTokens.append(( self.theTokens.append((
self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign self.T_SYNOPSIS, cLine[9:].strip(), None, self.A_NONE
)) ))
if self.doSynopsis: if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
else: else:
self.theTokens.append(( self.theTokens.append((
self.T_COMMENT, aLine[1:].strip(), None, defAlign self.T_COMMENT, aLine[1:].strip(), None, self.A_NONE
)) ))
if self.doComments: if self.doComments:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@": elif aLine[0] == "@":
self.theTokens.append(( self.theTokens.append((
self.T_KEYWORD, aLine[1:].strip(), None, self.A_LEFT self.T_KEYWORD, aLine[1:].strip(), None, self.A_NONE
)) ))
if self.doKeywords: if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ": elif aLine[:2] == "# ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT | self.A_PBB self.T_HEAD1, aLine[2:].strip(), None, self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ": elif aLine[:3] == "## ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT | self.A_PBA_AV self.T_HEAD2, aLine[3:].strip(), None, self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ": elif aLine[:4] == "### ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT | self.A_PBA_AV self.T_HEAD3, aLine[4:].strip(), None, self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ": elif aLine[:5] == "#### ":
self.theTokens.append(( self.theTokens.append((
self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT | self.A_PBA_AV self.T_HEAD4, aLine[5:].strip(), None, self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
@@ -367,13 +383,13 @@ class Tokenizer():
# sorted by position # sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0)) fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append(( self.theTokens.append((
self.T_TEXT, aLine, fmtPos, defAlign self.T_TEXT, aLine, fmtPos, self.A_NONE
)) ))
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end # Always add an empty line at the end
self.theTokens.append(( self.theTokens.append((
self.T_EMPTY, "", None, None self.T_EMPTY, "", None, self.A_NONE
)) ))
tmpMarkdown.append("\n") tmpMarkdown.append("\n")
@@ -391,8 +407,8 @@ class Tokenizer():
if self.isNone or self.isNote: if self.isNone or self.isNote:
return return
# For novel files, we need to handle chapter numbering and scene # For novel files, we need to handle chapter numbering, scene
# breaks # numbering, and scene breaks
if self.isNovel: if self.isNovel:
for n in range(len(self.theTokens)): for n in range(len(self.theTokens)):
@@ -410,7 +426,7 @@ class Tokenizer():
tText = self._formatHeading(self.fmtTitle, tText) tText = self._formatHeading(self.fmtTitle, tText)
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, None, self.A_LEFT | self.A_PBB_R tType, tText, None, self.A_NONE
) )
elif tType == self.T_HEAD2: elif tType == self.T_HEAD2:
@@ -426,7 +442,7 @@ class Tokenizer():
# Format the chapter header # Format the chapter header
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, None, self.A_LEFT | self.A_PBB_R tType, tText, None, self.A_PBB
) )
# Set scene variables # Set scene variables
@@ -443,21 +459,21 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtScene, tText) tTemp = self._formatHeading(self.fmtScene, tText)
if tTemp == "" and self.hideScene: if tTemp == "" and self.hideScene:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, None self.T_EMPTY, "", None, self.A_NONE
) )
elif tTemp == "" and not self.hideScene: elif tTemp == "" and not self.hideScene:
if self.firstScene: if self.firstScene:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, None self.T_EMPTY, "", None, self.A_NONE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_SKIP, "", None, None self.T_SKIP, "", None, self.A_NONE
) )
elif tTemp == self.fmtScene: elif tTemp == self.fmtScene:
if self.firstScene: if self.firstScene:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, None self.T_EMPTY, "", None, self.A_NONE
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
@@ -465,7 +481,7 @@ class Tokenizer():
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tTemp, None, self.A_LEFT | self.A_PBA_AV tType, tTemp, None, self.A_NONE
) )
# Definitely no longer the first scene # Definitely no longer the first scene
@@ -478,11 +494,11 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtSection, tText) tTemp = self._formatHeading(self.fmtSection, tText)
if tTemp == "" and self.hideSection: if tTemp == "" and self.hideSection:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_EMPTY, "", None, None self.T_EMPTY, "", None, self.A_NONE
) )
elif tTemp == "" and not self.hideSection: elif tTemp == "" and not self.hideSection:
self.theTokens[n] = ( self.theTokens[n] = (
self.T_SKIP, "", None, None self.T_SKIP, "", None, self.A_NONE
) )
elif tTemp == self.fmtSection: elif tTemp == self.fmtSection:
self.theTokens[n] = ( self.theTokens[n] = (
@@ -490,18 +506,28 @@ class Tokenizer():
) )
else: else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tTemp, None, self.A_LEFT | self.A_PBA_AV tType, tTemp, None, self.A_NONE
) )
# For title page and partitions, we need to centre all text. # For title page and partitions, we need to centre all text.
# For partition, we also add a page break before, and for # For partition, we also add a page break before, and for
# both types we always add a page break after the content. # both types we always add a page break after the content.
# We also swap header level 1 with a title type instead.
if self.isTitle or self.isPart: if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens): for n, tToken in enumerate(self.theTokens):
tType = tToken[0] tType = tToken[0]
tText = tToken[1] tText = tToken[1]
tFormat = tToken[2] tFormat = tToken[2]
if self.isTitle: if tType == self.T_HEAD1:
if self.isTitle:
self.theTokens[n] = (
self.T_TITLE, tText, tFormat, self.A_PBB_NO | self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tText, tFormat, self.A_PBB | self.A_CENTRE
)
else:
self.theTokens[n] = ( self.theTokens[n] = (
tType, tText, tFormat, self.A_CENTRE tType, tText, tFormat, self.A_CENTRE
) )
+96 -83
View File
@@ -34,7 +34,7 @@ from time import time
from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtCore import Qt, QByteArray
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextOption, QPalette, QColor, QTextDocumentWriter QTextOption, QPalette, QColor, QTextDocumentWriter, QFont
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
@@ -44,7 +44,7 @@ from PyQt5.QtWidgets import (
from nw.gui.additions import QSwitch from nw.gui.additions import QSwitch
from nw.core import ToHtml from nw.core import ToHtml
from nw.constants import ( from nw.constants import (
nwConst, nwFiles, nwAlert, nwItemType, nwItemLayout, nwItemClass nwAlert, nwItemType, nwItemLayout, nwItemClass
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -69,9 +69,9 @@ class GuiBuildNovel(QDialog):
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.optState = self.theProject.optState self.optState = self.theProject.optState
self.htmlText = [] # List of html document self.htmlText = [] # List of html document
self.nwdText = [] # List of markdown documents self.htmlStyle = [] # List of html styles
self.textLayout = [] # List of nwItemLayout entries self.nwdText = [] # List of markdown documents
self.setWindowTitle("Build Novel Project") self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(800) self.setMinimumWidth(800)
@@ -320,11 +320,23 @@ class GuiBuildNovel(QDialog):
tStart = time() tStart = time()
self.htmlText = [] self.htmlText = []
self.htmlStyle = []
self.nwdText = [] self.nwdText = []
self.textLayout = []
for nItt, tItem in enumerate(self.theProject.projTree): for nItt, tItem in enumerate(self.theProject.projTree):
if self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
noteRoot = noteFiles
noteRoot &= tItem.itemType == nwItemType.ROOT
noteRoot &= tItem.itemClass != nwItemClass.NOVEL
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())
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
makeHtml.setText(tItem.itemHandle) makeHtml.setText(tItem.itemHandle)
makeHtml.doAutoReplace() makeHtml.doAutoReplace()
makeHtml.tokenizeText() makeHtml.tokenizeText()
@@ -333,16 +345,17 @@ class GuiBuildNovel(QDialog):
makeHtml.doPostProcessing() makeHtml.doPostProcessing()
self.htmlText.append(makeHtml.getResult()) self.htmlText.append(makeHtml.getResult())
self.nwdText.append(makeHtml.getFilteredMarkdown()) self.nwdText.append(makeHtml.getFilteredMarkdown())
self.textLayout.append(tItem.itemLayout)
# Update progress bar, also for skipped items # Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1) self.buildProgress.setValue(nItt+1)
tEnd = time() tEnd = time()
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
self.htmlStyle = makeHtml.getStylesheet()
# Load the preview document with the html data # Load the preview document with the html data
self.docView.setHtml("".join(self.htmlText)) self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText)
return return
@@ -457,58 +470,53 @@ class GuiBuildNovel(QDialog):
return False return False
# Do the actual writing # Do the actual writing
wSuccess = False
errMsg = ""
if outTool == "Qt": if outTool == "Qt":
docWriter = QTextDocumentWriter() docWriter = QTextDocumentWriter()
docWriter.setFileName(savePath) docWriter.setFileName(savePath)
docWriter.setFormat(byteFmt) docWriter.setFormat(byteFmt)
if docWriter.write(self.docView.qDocument): wSuccess = docWriter.write(self.docView.qDocument)
self.theParent.makeAlert(
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write document in %s format to file: %s" % (
textFmt, savePath
), nwAlert.ERROR
)
elif outTool == "NW": elif outTool == "NW":
try: try:
with open(savePath, mode="w", encoding="utf8") as outFile: with open(savePath, mode="w", encoding="utf8") as outFile:
if theFormat == self.FMT_HTM: if theFormat == self.FMT_HTM:
# Write novelWriter HTML data # Write novelWriter HTML data
outFile.write("<!DOCTYPE html>\n") theStyle = self.htmlStyle.copy()
outFile.write("<html>\n") theStyle.append(r"article {width: 800px; margin: 40px auto;}")
outFile.write("<head>\n") theHtml = (
outFile.write("<meta charset='utf-8'>\n") "<!DOCTYPE html>\n"
outFile.write("</head>\n") "<html>\n"
outFile.write("<body>\n") "<head>\n"
outFile.write("<article style='width: 800px; margin: 40px auto'>\n") "<meta charset='utf-8'>\n"
for aLine in self.htmlText: "<title>{projTitle:s}</title>\n"
outFile.write(aLine) "</head>\n"
outFile.write("</article>\n") "<style>\n"
outFile.write("</body>\n") "{htmlStyle:s}\n"
outFile.write("</html>\n") "</style>\n"
"<body>\n"
"<article>\n"
"{bodyText:s}\n"
"</article>\n"
"</body>\n"
"</html>\n"
).format(
projTitle = self.theProject.projName,
htmlStyle = "\n".join(theStyle),
bodyText = "".join(self.htmlText),
)
outFile.write(theHtml)
elif theFormat == self.FMT_NWD: elif theFormat == self.FMT_NWD:
# Write novelWriter markdown data # Write novelWriter markdown data
for aLine in self.nwdText: for aLine in self.nwdText:
outFile.write(aLine) outFile.write(aLine)
self.theParent.makeAlert( wSuccess = True
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
except Exception as e: except Exception as e:
self.theParent.makeAlert( errMsg = str(e)
"Failed to write document in %s format to file: %s" % (
textFmt, str(e)
), nwAlert.ERROR
)
elif outTool == "QtPrint" and theFormat == self.FMT_PDF: elif outTool == "QtPrint" and theFormat == self.FMT_PDF:
try: try:
@@ -520,23 +528,29 @@ class GuiBuildNovel(QDialog):
thePrinter.setColorMode(QPrinter.Color) thePrinter.setColorMode(QPrinter.Color)
thePrinter.setOutputFileName(savePath) thePrinter.setOutputFileName(savePath)
self.docView.qDocument.print(thePrinter) self.docView.qDocument.print(thePrinter)
self.theParent.makeAlert( wSuccess = True
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
except Exception as e: except Exception as e:
self.theParent.makeAlert( errMsg - str(e)
"Failed to write document in %s format to file: %s" % (
textFmt, str(e)
), nwAlert.ERROR
)
else: else:
return False errMsg = "Unknown format"
return True # Report to user
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
)
return wSuccess
def _printDocument(self): def _printDocument(self):
"""Open the print preview dialog. """Open the print preview dialog.
@@ -550,7 +564,6 @@ class GuiBuildNovel(QDialog):
"""Connect the print preview painter to the document viewer. """Connect the print preview painter to the document viewer.
""" """
thePrinter.setOrientation(QPrinter.Portrait) thePrinter.setOrientation(QPrinter.Portrait)
thePrinter.setOutputFormat(QPrinter.NativeFormat | QPrinter.PdfFormat)
self.docView.qDocument.print(thePrinter) self.docView.qDocument.print(thePrinter)
return return
@@ -612,7 +625,8 @@ class GuiBuildNovel(QDialog):
if path.isfile(docPath): if path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile: with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read() helpText = inFile.read()
self.docView.setText(helpText) self.docView.setStyleSheet()
self.docView.setContent(helpText)
else: else:
self.theParent.makeAlert( self.theParent.makeAlert(
"Could not open help text file for Build Project.", nwAlert.ERROR "Could not open help text file for Build Project.", nwAlert.ERROR
@@ -638,6 +652,14 @@ class GuiBuildNovelDocView(QTextBrowser):
self.qDocument = self.document() self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin) self.qDocument.setDocumentMargin(self.mainConf.textMargin)
theFont = QFont()
if self.mainConf.textFont is None:
# If none is defined, set the default back to config
self.mainConf.textFont = self.qDocument.defaultFont().family()
theFont.setFamily(self.mainConf.textFont)
theFont.setPointSize(self.mainConf.textSize)
self.setFont(theFont)
theOpt = QTextOption() theOpt = QTextOption()
if self.mainConf.doJustify: if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify) theOpt.setAlignment(Qt.AlignJustify)
@@ -648,7 +670,7 @@ class GuiBuildNovelDocView(QTextBrowser):
docPalette.setColor(QPalette.Text, QColor( 0, 0, 0)) docPalette.setColor(QPalette.Text, QColor( 0, 0, 0))
self.setPalette(docPalette) self.setPalette(docPalette)
self._makeStyleSheet() self.setStyleSheet()
self.show() self.show()
@@ -656,35 +678,26 @@ class GuiBuildNovelDocView(QTextBrowser):
return return
def setText(self, theText): def setContent(self, theText):
"""Set the content, either from text or list of text.
"""
if isinstance(theText, list):
theText = "".join(theText)
theText = theText.replace("&emsp;","&nbsp;"*4)
self.setHtml(theText) self.setHtml(theText)
return return
## def setStyleSheet(self, theStyles=[]):
# Internal Functions """Set the stylesheet for the preview document.
## """
if not theStyles:
theStyles.append(r"h1, h2 {color: rgb(66, 113, 174);}")
theStyles.append(r"h3, h4 {color: rgb(50, 50, 50);}")
theStyles.append(r"a {color: rgb(137, 89, 168);}")
theStyles.append(r"mark {background-color: rgb(240, 198, 116);}")
theStyles.append(r".tags {color: rgb(245, 135, 31); font-weight: bold;}")
def _makeStyleSheet(self): self.qDocument.setDefaultStyleSheet("\n".join(theStyles))
styleSheet = (
"h1, h2 {"
" color: rgb(66, 113, 174);"
"}\n"
"h3, h4 {"
" color: rgb(50, 50, 50);"
"}\n"
"a {"
" color: rgb(137, 89, 168);"
"}\n"
"mark {"
" background-color: rgb(240, 198, 116);"
"}\n"
".tags {"
" color: rgb(245, 135, 31);"
" font-wright: bold;"
"}\n"
)
self.qDocument.setDefaultStyleSheet(styleSheet)
return return
@@ -0,0 +1,4 @@
%%~ edca4be2fcaf8:7031beac91f75:Part 1
# Part One
The first part.
+20 -7
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.5" hexVersion="0x000500f0" fileVersion="1.0" saveCount="130" autoCount="17" timeStamp="2020-05-11 19:23:21"> <novelWriterXML appVersion="0.5.2" hexVersion="0x000502f0" fileVersion="1.0" saveCount="158" autoCount="21" timeStamp="2020-05-23 21:12:53">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
@@ -10,7 +10,7 @@
<settings> <settings>
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>96b68994dfa3d</lastEdited> <lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>6a2d6d5f4f401</lastViewed> <lastViewed>6a2d6d5f4f401</lastViewed>
<lastWordCount>875</lastWordCount> <lastWordCount>875</lastWordCount>
<autoReplace> <autoReplace>
@@ -20,12 +20,12 @@
</autoReplace> </autoReplace>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
<chapter>Chapter %chnum%.\\%title%</chapter> <chapter>Chapter %chnum%: %title%</chapter>
<unnumbered>%title%</unnumbered> <unnumbered>%title%</unnumbered>
<scene>Scene %chnum%.%scnum%: %title%</scene> <scene>* * *</scene>
<section></section> <section></section>
<withSynopsis>True</withSynopsis> <withSynopsis>True</withSynopsis>
<withComments>False</withComments> <withComments>True</withComments>
<withKeywords>False</withKeywords> <withKeywords>False</withKeywords>
</titleFormat> </titleFormat>
<status> <status>
@@ -44,7 +44,7 @@
<entry blue="175" green="0" red="117">Main</entry> <entry blue="175" green="0" red="117">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="20"> <content count="21">
<item handle="7031beac91f75" order="0" parent="None"> <item handle="7031beac91f75" order="0" parent="None">
<name>Novel</name> <name>Novel</name>
<type>ROOT</type> <type>ROOT</type>
@@ -65,7 +65,20 @@
<paraCount>2</paraCount> <paraCount>2</paraCount>
<cursorPos>78</cursorPos> <cursorPos>78</cursorPos>
</item> </item>
<item handle="e7ded148d6e4a" order="1" parent="7031beac91f75"> <item handle="edca4be2fcaf8" order="1" parent="7031beac91f75">
<name>Part 1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>PARTITION</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="e7ded148d6e4a" order="2" parent="7031beac91f75">
<name>A Folder</name> <name>A Folder</name>
<type>FOLDER</type> <type>FOLDER</type>
<class>NOVEL</class> <class>NOVEL</class>