diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 0b786ad6..2b98be99 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -104,15 +104,24 @@ class ToHtml(Tokenizer): thisPar = [] for tType, tText, tFormat, tAlign in self.theTokens: + # Styles aStyle = [] if tAlign == self.A_CENTRE: aStyle.append("text-align: center;") + elif tAlign == self.A_RIGHT: + aStyle.append("text-align: right;") + elif tAlign == self.A_JUSTIFY: + aStyle.append("text-align: justify;") + + if tType == self.T_HEAD2: + aStyle.append("page-break-before: always;") if len(aStyle) > 0: hStyle = " style='%s'" % (" ".join(aStyle)) else: hStyle = "" + # Process TextType if tType == self.T_EMPTY: if len(thisPar) > 0: tTemp = "".join(thisPar) @@ -120,23 +129,30 @@ class ToHtml(Tokenizer): thisPar = [] elif tType == self.T_HEAD1: - self.theResult += "
%s
\n" % (hStyle,tText) + self.theResult += "%s
\n" % (hStyle, tText) elif tType == self.T_SKIP: self.theResult += "\n" + elif tType == self.T_PBREAK: + self.theResult += "
\n" + elif tType == self.T_TEXT: tTemp = tText for xPos, xLen, xFmt in reversed(tFormat): @@ -146,6 +162,9 @@ class ToHtml(Tokenizer): else: thisPar.append(tTemp.rstrip()+" ") + elif tType == self.T_SYNOPSIS and self.doSynopsis: + self.theResult += self._formatSynopsis(tText) + elif tType == self.T_COMMENT and self.doComments: self.theResult += self._formatComments(tText) @@ -158,12 +177,27 @@ class ToHtml(Tokenizer): # Internal Functions ## - def _formatKeywords(self, tText): - """Apply HTML formatting to keywords. + def _formatSynopsis(self, tText): + """Apply HTML formatting to synopsis. """ if not self.forPreview: - return "
@%s\n" % tText + return "
Synopsis: %s
\n" % tText + + return "%s
\n" % tText + + def _formatComments(self, tText): + """Apply HTML formatting to comments. + """ + + if not self.forPreview: + return "Comment: %s
\n" % tText + + return "%s
\n" % tText + + def _formatKeywords(self, tText): + """Apply HTML formatting to keywords. + """ tText = "@"+tText isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText) @@ -182,13 +216,4 @@ class ToHtml(Tokenizer): return "%s
\n" % tText - # END Class ToHtml diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 1cf2a40e..a39d12f2 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -40,29 +40,30 @@ logger = logging.getLogger(__name__) class Tokenizer(): - FMT_B_B = 1 # Begin bold - FMT_B_E = 2 # End bold - FMT_I_B = 3 # Begin italics - FMT_I_E = 4 # End italics - FMT_U_B = 5 # Begin underline - FMT_U_E = 6 # End underline + FMT_B_B = 1 # Begin bold + FMT_B_E = 2 # End bold + FMT_I_B = 3 # Begin italics + FMT_I_E = 4 # End italics + FMT_U_B = 5 # Begin underline + FMT_U_E = 6 # End underline - T_EMPTY = 1 # Empty line (new paragraph) - T_COMMENT = 2 # Comment line - T_KEYWORD = 3 # Command line - T_HEAD1 = 4 # Header 1 (title) - T_HEAD2 = 5 # Header 2 (chapter) - T_HEAD3 = 6 # Header 3 (scene) - T_HEAD4 = 7 # Header 4 - T_TEXT = 8 # Text line - T_SEP = 9 # Scene separator - T_SKIP = 10 # Paragraph break - T_PBREAK = 11 # Page break + T_EMPTY = 1 # Empty line (new paragraph) + T_SYNOPSIS = 2 # Synopsis comment + T_COMMENT = 3 # Comment line + T_KEYWORD = 4 # Command line + T_HEAD1 = 5 # Header 1 (title) + T_HEAD2 = 6 # Header 2 (chapter) + T_HEAD3 = 7 # Header 3 (scene) + T_HEAD4 = 8 # Header 4 + T_TEXT = 9 # Text line + T_SEP = 10 # Scene separator + T_SKIP = 11 # Paragraph break + T_PBREAK = 12 # Page break - A_LEFT = 1 # Left aligned - A_RIGHT = 2 # Right aligned - A_CENTRE = 3 # Centred - A_JUSTIFY = 4 # Justified + A_LEFT = 1 # Left aligned + A_RIGHT = 2 # Right aligned + A_CENTRE = 3 # Centred + A_JUSTIFY = 4 # Justified def __init__(self, theProject, theParent): @@ -78,8 +79,11 @@ class Tokenizer(): self.theResult = None # The result text after conversion # 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.fmtTitle = "%title%" # Formatting for titles self.fmtChapter = "%title%" # Formatting for numbered chapters @@ -113,14 +117,6 @@ class Tokenizer(): # Setters ## - def setComments(self, doComments): - self.doComments = doComments - return - - def setKeywords(self, doKeywords): - self.doKeywords = doKeywords - return - def setTitleFormat(self, fmtTitle): self.fmtTitle = fmtTitle return @@ -143,6 +139,26 @@ class Tokenizer(): self.hideSection = hideSection return + def setBodyText(self, doBodyText): + self.doBodyText = doBodyText + return + + def setSynopsis(self, doSynopsis): + self.doSynopsis = doSynopsis + return + + def setComments(self, doComments): + self.doComments = doComments + return + + def setKeywords(self, doKeywords): + self.doKeywords = doKeywords + return + + def setJustify(self, doJustify): + self.doJustify = doJustify + return + ## # Class Methods ## @@ -207,25 +223,38 @@ class Tokenizer(): [None, self.FMT_U_B, None, self.FMT_U_E] )] + if self.doJustify: + defAlign = self.A_JUSTIFY + else: + defAlign = self.A_LEFT + self.theTokens = [] for aLine in self.theText.splitlines(): # Tag lines starting with specific characters if len(aLine.strip()) == 0: - self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT)) + self.theTokens.append((self.T_EMPTY, "", None, self.A_LEFT)) elif aLine[0] == "%": - self.theTokens.append((self.T_COMMENT,aLine[1:].strip(),None,self.A_LEFT)) + cLine = aLine[1:].strip() + if cLine.lower().startswith("synopsis:"): + self.theTokens.append((self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign)) + else: + self.theTokens.append((self.T_COMMENT, aLine[1:].strip(), None, defAlign)) elif aLine[0] == "@": - self.theTokens.append((self.T_KEYWORD,aLine[1:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_KEYWORD, aLine[1:].strip(), None, self.A_LEFT)) elif aLine[:2] == "# ": - self.theTokens.append((self.T_HEAD1,aLine[2:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT)) elif aLine[:3] == "## ": - self.theTokens.append((self.T_HEAD2,aLine[3:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT)) elif aLine[:4] == "### ": - self.theTokens.append((self.T_HEAD3,aLine[4:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT)) elif aLine[:5] == "#### ": - self.theTokens.append((self.T_HEAD4,aLine[5:].strip(),None,self.A_LEFT)) + self.theTokens.append((self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT)) else: + if not self.doBodyText: + # Skip all body text + continue + # Otherwise we use RegEx to find formatting tags within a line of text fmtPos = [] for theRX, theKeys in rxFormats: @@ -240,11 +269,11 @@ class Tokenizer(): # Save the line as is, but append the array of formatting locations # sorted by position - fmtPos = sorted(fmtPos,key=itemgetter(0)) - self.theTokens.append((self.T_TEXT,aLine,fmtPos,self.A_LEFT)) + fmtPos = sorted(fmtPos, key=itemgetter(0)) + self.theTokens.append((self.T_TEXT, aLine, fmtPos, defAlign)) # Always add an empty line at the end - self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT)) + self.theTokens.append((self.T_EMPTY, "", None, self.A_LEFT)) return @@ -283,37 +312,37 @@ class Tokenizer(): if not isUnNum: self.numChapter += 1 tText = self._formatChapter(tText,isUnNum) - self.theTokens[n] = (tType,tText,None,self.A_LEFT) + self.theTokens[n] = (tType, tText, None, self.A_LEFT) self.firstScene = True elif tType == self.T_HEAD3: tTemp = self._formatScene(tText) if tTemp == "" and self.hideScene: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) elif tTemp == "" and not self.hideScene: if self.firstScene: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) else: - self.theTokens[n] = (self.T_SKIP,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_SKIP, "", None, self.A_LEFT) elif tTemp == self.fmtScene: if self.firstScene: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) else: - self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE) + self.theTokens[n] = (self.T_SEP, tTemp, None, self.A_CENTRE) else: - self.theTokens[n] = (tType,tTemp,None,self.A_LEFT) + self.theTokens[n] = (tType, tTemp, None, self.A_LEFT) self.firstScene = False elif tType == self.T_HEAD4: tTemp = self._formatSection(tText) if tTemp == "" and self.hideSection: - self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_EMPTY, "", None, self.A_LEFT) elif tTemp == "" and not self.hideSection: - self.theTokens[n] = (self.T_SKIP,"",None,self.A_LEFT) + self.theTokens[n] = (self.T_SKIP, "", None, self.A_LEFT) elif tTemp == self.fmtSection: - self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE) + self.theTokens[n] = (self.T_SEP, tTemp, None, self.A_CENTRE) else: - self.theTokens[n] = (tType,tTemp,None,self.A_LEFT) + self.theTokens[n] = (tType, tTemp, None, self.A_LEFT) # For title page and partitions, we need to centre all text # and for some formats, we need a page break @@ -323,9 +352,9 @@ class Tokenizer(): tType = tToken[0] tText = tToken[1] tFormat = tToken[2] - self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE) + self.theTokens[n] = (tType, tText, tFormat, self.A_CENTRE) - self.theTokens.append((self.T_PBREAK,"",None,self.A_LEFT)) + self.theTokens.append((self.T_PBREAK, "", None, self.A_LEFT)) return diff --git a/nw/gui/build.py b/nw/gui/build.py index 94c82019..2e1e5f0f 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -42,7 +42,9 @@ from PyQt5.QtWidgets import ( from nw.gui.additions import QSwitch from nw.core import ToHtml -from nw.constants import nwConst, nwFiles, nwAlert, nwItemType +from nw.constants import ( + nwConst, nwFiles, nwAlert, nwItemType, nwItemLayout, nwItemClass +) logger = logging.getLogger(__name__) @@ -70,11 +72,11 @@ class GuiBuildNovel(QDialog): self.setWindowTitle("Build Project") self.setMinimumWidth(800) - self.setMinimumHeight(700) + self.setMinimumHeight(800) self.resize( self.optState.getInt("GuiBuildNovel", "winWidth", 800), - self.optState.getInt("GuiBuildNovel", "winHeight", 700) + self.optState.getInt("GuiBuildNovel", "winHeight", 800) ) self.outerBox = QVBoxLayout() @@ -128,6 +130,21 @@ class GuiBuildNovel(QDialog): self.titleForm.setColumnStretch(0, 1) self.titleForm.setColumnStretch(1, 0) + # Text Options + # ============= + self.textGroup = QGroupBox("Text Options", self) + self.textForm = QGridLayout(self) + self.textGroup.setLayout(self.textForm) + + self.justifyText = QSwitch() + self.justifyText.setChecked(self.optState.getBool("GuiBuildNovel", "justifyText", False)) + + self.textForm.addWidget(QLabel("Justify text"), 0, 0) + self.textForm.addWidget(self.justifyText, 0, 1) + + self.textForm.setColumnStretch(0, 1) + self.textForm.setColumnStretch(1, 0) + # Build Settings # ============== self.buildGroup = QGroupBox("Build Overrides", self) @@ -156,11 +173,11 @@ class GuiBuildNovel(QDialog): self.includeKeywords = QSwitch() self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"]) - self.includeForm.addWidget(QLabel("Include Synopsis"), 0, 0) + self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0) self.includeForm.addWidget(self.includeSynopsis, 0, 1) - self.includeForm.addWidget(QLabel("Include Comments"), 1, 0) + self.includeForm.addWidget(QLabel("Include comments"), 1, 0) self.includeForm.addWidget(self.includeComments, 1, 1) - self.includeForm.addWidget(QLabel("Include Keywords"), 2, 0) + self.includeForm.addWidget(QLabel("Include keywords"), 2, 0) self.includeForm.addWidget(self.includeKeywords, 2, 1) self.includeForm.setColumnStretch(0, 1) @@ -179,11 +196,11 @@ class GuiBuildNovel(QDialog): self.ignoreFlag = QSwitch() self.ignoreFlag.setChecked(self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)) - self.addsForm.addWidget(QLabel("Include Novel Files"), 0, 0) + self.addsForm.addWidget(QLabel("Include novel files"), 0, 0) self.addsForm.addWidget(self.novelFiles, 0, 1) - self.addsForm.addWidget(QLabel("Include Note Files"), 1, 0) + self.addsForm.addWidget(QLabel("Include note files"), 1, 0) self.addsForm.addWidget(self.noteFiles, 1, 1) - self.addsForm.addWidget(QLabel("Ignore Export Flag"), 2, 0) + self.addsForm.addWidget(QLabel("Ignore export flag"), 2, 0) self.addsForm.addWidget(self.ignoreFlag, 2, 1) self.addsForm.setColumnStretch(0, 1) @@ -245,6 +262,7 @@ class GuiBuildNovel(QDialog): # Assemble GUI # ============ self.toolsBox.addWidget(self.titleGroup) + self.toolsBox.addWidget(self.textGroup) self.toolsBox.addWidget(self.buildGroup) self.toolsBox.addWidget(self.includeGroup) self.toolsBox.addWidget(self.addsGroup) @@ -280,11 +298,50 @@ class GuiBuildNovel(QDialog): """Build a preview of the project in the document viewer. """ - makeHtml = ToHtml(self.theProject, self.theParent) - self.htmlText = "" + # 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() + outlineMode = self.outlineMode.isChecked() + incSynopsis = self.includeSynopsis.isChecked() + incComments = self.includeComments.isChecked() + incKeywords = self.includeKeywords.isChecked() + novelFiles = self.novelFiles.isChecked() + noteFiles = self.noteFiles.isChecked() + ignoreFlag = self.ignoreFlag.isChecked() + doBodyText = True - for tItem in self.theProject.projTree: - if tItem is not None and tItem.itemType == nwItemType.FILE: + if outlineMode: + fmtTitle = "%title%" + fmtChapter = "Chapter: %title%" + fmtUnnumbered = "Chapter: %title%" + fmtScene = "Scene: %title%" + fmtSection = "Section: %title%" + doBodyText = False + incSynopsis = True + novelFiles = True + noteFiles = False + + 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(doBodyText) + makeHtml.setSynopsis(incSynopsis) + makeHtml.setComments(incComments) + makeHtml.setKeywords(incKeywords) + makeHtml.setJustify(justifyText) + + self.htmlText = "" + self.buildProgress.setMaximum(len(self.theProject.projTree)) + self.buildProgress.setValue(0) + for nItt, tItem in enumerate(self.theProject.projTree): + if self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): makeHtml.setText(tItem.itemHandle) makeHtml.doAutoReplace() makeHtml.tokenizeText() @@ -292,11 +349,49 @@ class GuiBuildNovel(QDialog): makeHtml.doConvert() makeHtml.doPostProcessing() self.htmlText += makeHtml.getResult() + self.buildProgress.setValue(nItt+1) self.docView.setHtml(self.htmlText) return + def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag): + """This function checks whether a file should be included in the + export or not. For standard note and novel files, this is + controlled by the options selected by the user. For other files + classified as non-exportable, a few checks must be made, and the + following are not: + * Items that are not actual files. + * Items that have been orphaned which are tagged as NO_LAYOUT + and NO_CLASS. + * Items that appear in the TRASH folder or have parent set to + None (orphaned files). + """ + + if theItem is None: + return False + + if not theItem.isExported and not ignoreFlag: + return False + + isNone = theItem.itemType != nwItemType.FILE + isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT + isNone |= theItem.itemClass == nwItemClass.NO_CLASS + isNone |= theItem.itemClass == nwItemClass.TRASH + isNone |= theItem.parHandle == self.theProject.projTree.trashRoot() + isNone |= theItem.parHandle is None + isNote = theItem.itemLayout == nwItemLayout.NOTE + isNovel = not isNone and not isNote + + if isNone: + return False + if isNote and not noteFiles: + return False + if isNovel and not novelFiles: + return False + + return True + def _saveDocument(self, theFormat): """Save the document to various formats. """ @@ -474,6 +569,7 @@ class GuiBuildNovel(QDialog): # GUI Settings self.optState.setValue("GuiBuildNovel", "winWidth", self.width()) self.optState.setValue("GuiBuildNovel", "winHeight", self.height()) + self.optState.setValue("GuiBuildNovel", "justifyText", self.justifyText.isChecked()) self.optState.setValue("GuiBuildNovel", "outlineMode", self.outlineMode.isChecked()) self.optState.setValue("GuiBuildNovel", "addNovel", self.novelFiles.isChecked()) self.optState.setValue("GuiBuildNovel", "addNotes", self.noteFiles.isChecked()) @@ -557,6 +653,10 @@ class GuiBuildNovelDocView(QTextBrowser): "mark {" " background-color: rgb(240, 198, 116);" "}\n" + ".tags {" + " color: rgb(245, 135, 31);" + " font-wright: bold;" + "}\n" ) self.qDocument.setDefaultStyleSheet(styleSheet) diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 69ac815d..7b76aea0 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ -