diff --git a/nw/convert/textfile.py b/nw/convert/textfile.py new file mode 100644 index 00000000..04cd4b3a --- /dev/null +++ b/nw/convert/textfile.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +"""novelWriter Text File + + novelWriter – Text File +========================= + Writes the project to a plain text file + + File History: + Created: 2019-10-18 [0.2.3] + +""" + +import logging +import nw + +from os import path +from PyQt5.QtWidgets import QMessageBox + +from nw.convert.tokenizer import Tokenizer +from nw.enum import nwAlert + +logger = logging.getLogger(__name__) + +class TextFile(): + + def __init__(self, theProject, theParent): + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + self.fileExt = "txt" + + self.outFile = None + self.fileName = "" + self.theText = "" + self.doComments = False + self.doMeta = False + self.wordWrap = 80 + self.winEnding = False + + self.makeAlert = self.theParent.makeAlert + + return + + ## + # Setters + ## + + def setComments(self, doComments): + self.doComments = doComments + return + + def setMeta(self, doMeta): + self.doMeta = doMeta + return + + ## + # Core Methods + ## + + def openFile(self, saveTo, baseName): + + fileName = "%s.%s" % (baseName.strip(),self.fileExt) + filePath = path.join(saveTo,fileName) + + self.fileName = fileName + + if path.isfile(filePath)and self.mainConf.showGUI: + msgBox = QMessageBox() + msgRes = msgBox.question( + self.theParent, "Overwrite", ("File '%s' already exists.
Do you want to overwrite it?" % fileName) + ) + if msgRes != QMessageBox.Yes: + return False + + self._doOpenFile(filePath) + + return True + + def closeFile(self): + self._doCloseFile() + return True + + def addText(self, tHandle): + + logger.verbose("Parsing content of item '%s'" % tHandle) + + aDoc = Tokenizer(self.theProject, self.theParent) + aDoc.setText(tHandle) + aDoc.doAutoReplace() + aDoc.tokenizeText() + + aDoc.setComments(self.doComments) + aDoc.setCommands(self.doMeta) + aDoc.setWordWrap(self.wordWrap) + + aDoc.doConvert() + + theText = "" + if aDoc.theResult is not None: + theText = aDoc.theResult + + if self.winEnding: + theText = theText.replace("\n","\r\n") + + self.outFile.write(theText) + + return True + + ## + # Internal Functions + ## + + def _doOpenFile(self, filePath): + try: + self.outFile = open(filePath,mode="w+") + except Exception as e: + self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) + return False + return True + + def _doCloseFile(self): + self.outFile.close() + return True + +# END Class OutFile diff --git a/nw/convert/tokenizer.py b/nw/convert/tokenizer.py index 0d62175f..bbb7877b 100644 --- a/nw/convert/tokenizer.py +++ b/nw/convert/tokenizer.py @@ -10,6 +10,7 @@ """ +import textwrap import logging import re import nw @@ -41,6 +42,25 @@ class Tokenizer(): self.theTokens = None self.theResult = None + self.wordWrap = 80 + self.doComments = False + self.doCommands = False + + return + + def setComments(self, doComments): + self.doComments = doComments + return + + def setCommands(self, doCommands): + self.doCommands = doCommands + return + + def setWordWrap(self, wordWrap): + if wordWrap >= 0: + self.wordWrap = wordWrap + else: + self.wordWrap = 0 return def setText(self, theHandle, theText=None): @@ -129,4 +149,61 @@ class Tokenizer(): return + def doConvert(self): + """Converts the tokenized text into plain text. + """ + + self.theResult = "" + thisPar = [] + for tType, tText, tFormat in self.theTokens: + + # First check if we have a comment or plain text, as they need some + # extra replacing before we proceed + if tType == "comment": + tText = "[%s]" % tText + + elif tType == "text": + tTemp = tText + for xPos, xLen, xFmt in reversed(tFormat): + tTemp = tTemp[:xPos]+tTemp[xPos+xLen:] + tText = tTemp + + # The text can now be word wrapped, if we have requested this + if self.wordWrap > 0: + tText = textwrap.fill(tText, width=self.wordWrap) + + # Then the text can receive final formatting before we append it + # to the results. We store text bits in a buffer and merge them only + # when we find an empty line, indicating a new paragraph + if tType == "empty": + if len(thisPar) > 0: + self.theResult += "%s\n\n" % " ".join(thisPar) + thisPar = [] + + elif tType == "header1": + uLine = "="*min(len(tText),self.wordWrap) + self.theResult += "%s\n%s\n\n" % (tText,uLine) + + elif tType == "header2": + uLine = "~"*min(len(tText),self.wordWrap) + self.theResult += "%s\n%s\n\n" % (tText,uLine) + + elif tType == "header3": + uLine = "-"*min(len(tText),self.wordWrap) + self.theResult += "%s\n%s\n\n" % (tText,uLine) + + elif tType == "header4": + self.theResult += "%s\n\n" % tText + + elif tType == "text": + thisPar.append(tText) + + elif tType == "comment" and self.doComments: + self.theResult += "%s\n\n" % tText + + elif tType == "command" and self.doCommands: + self.theResult += "%s\n\n" % tText + + return + # END Class Tokenizer diff --git a/nw/gui/export.py b/nw/gui/export.py index 995666f3..0c78211d 100644 --- a/nw/gui/export.py +++ b/nw/gui/export.py @@ -11,6 +11,7 @@ """ import logging +import time import json import nw @@ -19,13 +20,16 @@ from os import path from PyQt5.QtCore import Qt, QSize from PyQt5.QtSvg import QSvgWidget from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QDialogButtonBox, QGridLayout, - QGroupBox, QCheckBox, QLabel, QComboBox, QLineEdit, QPushButton, QFileDialog + QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QGridLayout, QGroupBox, QCheckBox, + QLabel, QComboBox, QLineEdit, QPushButton, QFileDialog, QProgressBar ) -from nw.tools.translate import numberToWord -from nw.common import checkString, checkBool, checkInt -from nw.constants import nwFiles +from nw.project.document import NWDoc +from nw.tools.translate import numberToWord +from nw.convert.textfile import TextFile +from nw.common import checkString, checkBool, checkInt +from nw.constants import nwFiles +from nw.enum import nwItemType logger = logging.getLogger(__name__) @@ -59,13 +63,27 @@ class GuiExport(QDialog): self.outerBox.addWidget(self.svgGradient, 0, Qt.AlignTop) self.outerBox.addLayout(self.innerBox) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self._doClose) + self.doExportForm = QGridLayout() + self.doExportForm.setContentsMargins(10,5,0,10) + + self.exportButton = QPushButton("Export") + self.exportButton.clicked.connect(self._doExport) + + self.closeButton = QPushButton("Close") + self.closeButton.clicked.connect(self._doClose) + + self.exportStatus = QLabel("Ready ...") + self.exportProgress = QProgressBar(self) + + self.doExportForm.addWidget(self.exportStatus, 0, 0, 1, 3) + self.doExportForm.addWidget(self.exportProgress, 1, 0) + self.doExportForm.addWidget(self.exportButton, 1, 1) + self.doExportForm.addWidget(self.closeButton, 1, 2) self.innerBox.addWidget(self.tabWidget) - self.innerBox.addWidget(self.buttonBox) + self.innerBox.addLayout(self.doExportForm) + self.rejected.connect(self._doClose) self.show() logger.debug("GuiExport initialisation complete") @@ -76,9 +94,56 @@ class GuiExport(QDialog): # Buttons ## - def _doSave(self): + def _doExport(self): - logger.verbose("GuiExport save button clicked") + logger.verbose("GuiExport export button clicked") + + eFormat = self.tabMain.outputFormat.currentData() + wComments = self.tabMain.outputComments.isChecked() + saveTo = self.tabMain.exportPath.text() + + outFile = None + if eFormat == GuiExportMain.FMT_TXT: + outFile = TextFile(self.theProject, self.theParent) + + if outFile is None: + return False + + outFile.openFile(saveTo,"testfile") + outFile.setComments(wComments) + + nItems = len(self.theProject.treeOrder) + self.exportProgress.setMinimum(0) + self.exportProgress.setMaximum(nItems) + self.exportProgress.setValue(0) + + nDone = 0 + for tHandle in self.theProject.treeOrder: + + time.sleep(0.1) + + self.exportProgress.setValue(nDone) + tItem = self.theProject.getItem(tHandle) + + self.exportStatus.setText("Exporting: %s" % tItem.itemName) + logger.verbose("Exporting: %s" % tItem.itemName) + + if tItem is not None and tItem.itemType == nwItemType.FILE: + outFile.addText(tHandle) + + nDone += 1 + + outFile.closeFile() + + self.exportProgress.setValue(nDone) + self.exportStatus.setText("Export to %s complete" % outFile.fileName) + logger.verbose("Export to %s complete" % outFile.fileName) + + return + + def _doClose(self): + + logger.verbose("GuiExport close button clicked") wNovel = self.tabMain.expNovel.isChecked() wNotes = self.tabMain.expNotes.isChecked() @@ -103,21 +168,21 @@ class GuiExport(QDialog): return - def _doClose(self): - logger.verbose("GuiExport close button clicked") - self.close() - return - # END Class GuiExport class GuiExportMain(QWidget): - FMT_MD = 1 - FMT_HTML = 2 - FMT_EBOOK = 3 - FMT_ODT = 4 - FMT_TEX = 5 + FMT_TXT = 1 + FMT_MD = 2 + FMT_HTML = 3 + FMT_EBOOK = 4 + FMT_ODT = 5 + FMT_TEX = 6 FMT_HELP = { + FMT_TXT : ( + "Exports a plain text file. " + "All formatting is stripped and comments are in square brackets." + ), FMT_MD : ( "Exports a standard markdown file. " "Comments are converted to preformatted text blocks." @@ -198,11 +263,12 @@ class GuiExportMain(QWidget): self.outputHelp.setAlignment(Qt.AlignTop) self.outputFormat = QComboBox(self) - self.outputFormat.addItem("Markdown", self.FMT_MD) - self.outputFormat.addItem("HTML5 (Plain)", self.FMT_HTML) - self.outputFormat.addItem("HTML5 (eBook)", self.FMT_EBOOK) - self.outputFormat.addItem("Open Document", self.FMT_ODT) - self.outputFormat.addItem("LaTeX (PDF)", self.FMT_TEX) + self.outputFormat.addItem("Plain Text", self.FMT_TXT) + # self.outputFormat.addItem("Markdown", self.FMT_MD) + # self.outputFormat.addItem("HTML5 (Plain)", self.FMT_HTML) + # self.outputFormat.addItem("HTML5 (eBook)", self.FMT_EBOOK) + # self.outputFormat.addItem("Open Document", self.FMT_ODT) + # self.outputFormat.addItem("LaTeX (PDF)", self.FMT_TEX) self.outputFormat.currentIndexChanged.connect(self._updateFormatHelp) optIdx = self.outputFormat.findData(self.optState.eFormat()) @@ -300,7 +366,7 @@ class ExportLastState(): "wNovel" : True, "wNotes" : False, "wTOC" : True, - "eFormat" : 1, + "eFormat" : 2, "wComments" : False, "chFormat" : "Chapter %numword%", "scFormat" : "* * *",