Merge pull request #204 from vkbo/compile_novel

Compile Novel Tools
This commit is contained in:
Veronica K. Berglyd Olsen
2020-05-12 20:07:29 +02:00
committed by GitHub
48 changed files with 1936 additions and 2387 deletions
+49
View File
@@ -0,0 +1,49 @@
<h1>Help!</h1>
<p><i>A brief guide to make the most out of the Build Novel Project tool.</i></p>
<h2>Novel Title Formats</h2>
<p>The format of the various title levels in the files under the Novel folder can be customised in
these settings. The actual title given in the headings of your files will for instance replace
all occurrences of the keyword <mark>%title%</mark>. Any static text will be left as-is in the
final title. An empty field means the title isn't written out at all.</p>
<p>The available formatting keywords are:</p>
<p><mark>%title%</mark> &ndash; This is replaced with the text you put in your headings in your
documents</p>
<p><mark>%chnum%</mark> &ndash; This is replaced with the chapter number of your chapter type
headings. These are generated automaticall starting from 1, but ignoring chapter headings in
files with "Unnumbered" layout.</p>
<p><mark>%chnumword%</mark> &ndash; This is replaced with the chapter number, but instead of an
arabic number, the word for it is used, e.g. One, Two, Fifteen, Twenty-Five, etc.</p>
<p><mark>%scnum%</mark> &ndash; This is replaced with the scene number. The number is reset to one
for each new chapter, so it is the scene number within the current chapter.</p>
<p><mark>%scabsnum%</mark> &ndash; This is replaced with the absolute scene number. That is, the
number is counted from the first scene in the novel, and not reset for each chapter.</p>
<p><mark>\\</mark> &ndash; Two backslashes are replaced by a line break.</p>
<p><b>Note:</b> The Scene and Section formats are treated slightly differently than the other title
formats. If the format is a constant text, that is, contains no <mark>%keyword%</mark> tags, it
will be treated as a separator instead. Scene and Section separators are centred, and for
scenes, not shown if placed directly after the chapter heading. For instance, it you want the
classic three asterisk <mark>* * *</mark> separator between scenes, just put that into the
scene format box, and nothing else.</p>
<h2>Build Overrides</h2>
<p><b>Novel Outline Mode:</b> This option will build an outline version of the novel rather than the
full thing. It overrides the title format settings without changing them. Each title will be
written out, and the synopsis text will appear instead of the body text of the files. Some of
the other options are still available in Outline Mode.</p>
<h2>Include Non-Text Elements</h2>
<p><b>Include Synopsis:</b> This will add the synopsis comment as the first paragraph after each
heading.</p>
<p><b>Include Comments:</b> This will include any comments as additional paragraphs in the text.</p>
<p><b>Include Keywords:</b> This will include any keywords and tags as clickable links after each
heading.</p>
<h2>Additional Options</h2>
<p><b>Include Novel Files:</b> This means all files that don't have a layout of type "Note" will be
included. This is the normal mode when exporting the novel itself without the notes.</p>
<p><b>Include Note Files:</b> This means all files with a layout of type "Note" <i>will</i> be
included. Titles in note files are always left as they appear.</p>
<p><b>Ignore Export Flag:</b> Each file in the project tree has an "Include when building project"
option set, which is indicated by a little check mark in the "Flags" column. Files without This
tick will normally be skipped during build, but can be included if this option is enabled.</p>
-8
View File
@@ -172,11 +172,3 @@ def splitVersionNumber(vString):
vInt = vMajor*10000 + vMinor*100 + vPatch
return [vMajor, vMinor, vPatch, vInt]
def packageRefURL(packName):
from nw.constants import nwDependencies
if packName in nwDependencies.PACKS.keys():
return "<a href=\"%s\">%s</a>" % (
nwDependencies.PACKS[packName]["site"], packName
)
return packName
+1
View File
@@ -84,6 +84,7 @@ class Config:
self.guiTheme = "default"
self.guiSyntax = "default_light"
self.guiDark = False
self.guiLang = "en" # Hardcoded for now
## Sizes
self.winGeometry = [1100, 650]
+1 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from nw.constants.iso import isoLanguage, isoCountry
from nw.constants.constants import (
nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode
nwConst, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode
)
from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline
@@ -14,7 +14,6 @@ __all__ = [
"nwFiles",
"nwKeyWords",
"nwLabels",
"nwDependencies",
"nwQuotes",
"nwUnicode",
"nwAlert",
-28
View File
@@ -142,34 +142,6 @@ class nwLabels():
# END Class nwLabels
class nwDependencies():
"""Python package dependencies and their reference links.
"""
PACKS = {
"pyqt5" : {
"site" : "",
"docs" : "",
},
"lxml" : {
"site" : "",
"docs" : "",
},
"pyenchant" : {
"site" : "",
"docs" : "",
},
"latexcodec" : {
"site" : "https://pypi.org/project/latexcodec/",
"docs" : "https://latexcodec.readthedocs.io/en/latest/",
},
"pypandoc" : {
"site" : "https://pypi.org/project/pypandoc/",
"docs" : "https://pypi.org/project/pypandoc/",
},
}
# END Class nwDependencies
class nwQuotes():
"""Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark
-27
View File
@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
from nw.convert.tokenizer import Tokenizer
from nw.convert.file.concat import ConcatFile
from nw.convert.file.html import HtmlFile
from nw.convert.file.latex import LaTeXFile
from nw.convert.file.markdown import MarkdownFile
from nw.convert.file.text import TextFile
from nw.convert.text.tohtml import ToHtml
from nw.convert.text.tolatex import ToLaTeX
from nw.convert.text.tomarkdown import ToMarkdown
from nw.convert.text.totext import ToText
__all__ = [
"Tokenizer",
"ConcatFile",
"HtmlFile",
"LaTeXFile",
"MarkdownFile",
"TextFile",
"ToHtml",
"ToLaTeX",
"ToMarkdown",
"ToText",
]
-15
View File
@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from nw.convert.file.concat import ConcatFile
from nw.convert.file.html import HtmlFile
from nw.convert.file.latex import LaTeXFile
from nw.convert.file.markdown import MarkdownFile
from nw.convert.file.text import TextFile
__all__ = [
"ConcatFile",
"HtmlFile",
"LaTeXFile",
"MarkdownFile",
"TextFile",
]
-80
View File
@@ -1,80 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Concatenated File
novelWriter Concatenated File
=================================
Concatenate the standard novelWriter files to a single file
File History:
Created: 2019-10-26 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from nw.convert.file.text import TextFile
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class ConcatFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = Tokenizer(self.theProject, self.theParent)
return
def addText(self, tHandle):
logger.verbose("Parsing content of item '%s'" % tHandle)
if not self.checkInclude(tHandle):
return False
self.theConv.setText(tHandle)
theResult = self.theConv.theText
if theResult is not None and self.outFile is not None:
self.outFile.write(theResult.rstrip())
self.outFile.write("\n\n")
return True
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.close()
return True
# END Class ConcatFile
-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter HTML File
novelWriter HTML File
=========================
Writes the project to a html file
File History:
Created: 2019-10-19 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from nw.convert.file.text import TextFile
from nw.convert.text.tohtml import ToHtml
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class HtmlFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = ToHtml(self.theProject, self.theParent)
return
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
self.outFile.write("<!DOCTYPE html>\n")
self.outFile.write("<html>\n")
self.outFile.write("<head>\n")
self.outFile.write(" <meta charset='utf-8'>\n")
self.outFile.write(" <style>\n")
self.outFile.write(" #page {\n")
self.outFile.write(" margin: 40px auto;\n")
self.outFile.write(" max-width: 769px;\n")
self.outFile.write(" }\n")
self.outFile.write(" .comment {\n")
self.outFile.write(" background-color: #fbfabd;\n")
self.outFile.write(" border: 1px solid #b4b000;\n")
self.outFile.write(" margin: 10px 20px;\n")
self.outFile.write(" padding: 6px;\n")
self.outFile.write(" }\n")
self.outFile.write(" </style>\n")
self.outFile.write("</head>\n")
self.outFile.write("<body>\n")
self.outFile.write("<article id='page'>\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.write("</article>\n")
self.outFile.write("</body>\n")
self.outFile.write("</html>\n")
self.outFile.close()
return True
# END Class HtmlFile
-69
View File
@@ -1,69 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter LaTeX File
novelWriter LaTeX File
==========================
Writes the project to a LaTeX file
File History:
Created: 2019-10-24 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from nw.convert.file.text import TextFile
from nw.convert.text.tolatex import ToLaTeX
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class LaTeXFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = ToLaTeX(self.theProject, self.theParent)
self.texCodecFail = False
return
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
self.outFile.write("\\documentclass[12pt]{report}\n")
self.outFile.write("\\usepackage[utf8]{inputenc}\n")
self.outFile.write("\\usepackage[T1]{fontenc}\n")
self.outFile.write("\n")
self.outFile.write("\\begin{document}\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.write("\\end{document}\n")
self.outFile.close()
self.texCodecFail = self.theConv.texCodecFail
return True
# END Class LaTeXFile
-61
View File
@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Markdown File
novelWriter Markdown File
=============================
Writes the project to a markdown file
File History:
Created: 2019-10-19 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from nw.convert.file.text import TextFile
from nw.convert.text.tomarkdown import ToMarkdown
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class MarkdownFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = ToMarkdown(self.theProject, self.theParent)
return
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.close()
return True
# END Class MarkdownFile
-210
View File
@@ -1,210 +0,0 @@
# -*- 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]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from PyQt5.QtWidgets import QMessageBox
from nw.convert.text.totext import ToText
from nw.constants import nwAlert, nwItemType, nwItemLayout, nwItemClass
logger = logging.getLogger(__name__)
class TextFile():
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.outFile = None
self.fileName = ""
self.theText = ""
self.expNovel = True
self.expNotes = False
self.theConv = ToText(self.theProject, self.theParent)
self.makeAlert = self.theParent.makeAlert
self.setComments(False)
self.setKeywords(False)
self.setWordWrap(80)
return
##
# Setters
##
def setExportNovel(self, doNovel):
self.expNovel = doNovel
return
def setExportNotes(self, doNotes):
self.expNotes = doNotes
return
def setComments(self, doComments):
self.theConv.setComments(doComments)
return
def setKeywords(self, doKeywords):
self.theConv.setKeywords(doKeywords)
return
def setWordWrap(self, wordWrap):
if wordWrap >= 0:
self.theConv.setWordWrap(wordWrap)
else:
self.theConv.setWordWrap(0)
return
def setTitleFormat(self, fmtTitle):
self.theConv.setTitleFormat(fmtTitle)
return
def setChapterFormat(self, fmtChapter):
self.theConv.setChapterFormat(fmtChapter)
return
def setUnNumberedFormat(self, fmtUnNum):
self.theConv.setUnNumberedFormat(fmtUnNum)
return
def setSceneFormat(self, fmtScene, hideScene):
self.theConv.setSceneFormat(fmtScene, hideScene)
return
def setSectionFormat(self, fmtSection, hideSection):
self.theConv.setSectionFormat(fmtSection, hideSection)
return
##
# Core Methods
##
def openFile(self, filePath):
self.fileName = path.basename(filePath)
if path.isfile(filePath) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Overwrite", (
"File '%s' already exists.<br>Do you want to overwrite it?" % self.fileName
))
if msgRes != QMessageBox.Yes:
return False
self._doOpenFile(filePath)
if self.outFile is None:
return False
return True
def closeFile(self):
self._doCloseFile()
return True
def addText(self, tHandle):
logger.verbose("Parsing content of item '%s'" % tHandle)
if not self.checkInclude(tHandle):
return False
self.theConv.setText(tHandle)
self.theConv.doAutoReplace()
self.theConv.tokenizeText()
self.theConv.doHeaders()
self.theConv.doConvert()
self.theConv.doPostProcessing()
if self.theConv.theResult is not None and self.outFile is not None:
self.outFile.write(self.theConv.theResult)
return True
def checkInclude(self, tHandle):
"""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
"""
theItem = self.theProject.projTree[tHandle]
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()
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
if isNone:
return False
if isNote and not self.expNotes:
return False
if isNovel and not self.expNovel:
return False
return True
##
# Internal Functions
##
def _doOpenFile(self, filePath):
"""This function does the actual opening of the file, and can be
overloaded by a subclass that uses a different file format that
requires a different approach.
"""
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
self.outFile.write("\n\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
"""This function closes the file, and is meant to be overloaded
by the subclass for other file formats.
"""
if self.outFile is not None:
self.outFile.close()
return True
# END Class OutFile
-13
View File
@@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from nw.convert.text.tohtml import ToHtml
from nw.convert.text.tolatex import ToLaTeX
from nw.convert.text.tomarkdown import ToMarkdown
from nw.convert.text.totext import ToText
__all__ = [
"ToHtml",
"ToLaTeX",
"ToMarkdown",
"ToText",
]
-176
View File
@@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter HTML Text Converter
novelWriter HTML Text Converter
===================================
Extends the Tokenizer class to write HTML
File History:
Created: 2019-05-07 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwUnicode, nwLabels
logger = logging.getLogger(__name__)
class ToHtml(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.forPreview = False
return
def setPreview(self, forPreview, doComments):
"""If we're using this class to generate markdown preview, we
need to make a few changes to formatting, which is selected by
this flag.
"""
self.forPreview = forPreview
if forPreview:
self.doKeywords = True
self.doComments = doComments
return
def doAutoReplace(self):
Tokenizer.doAutoReplace(self)
if self.forPreview:
tabFmt = "&nbsp;"*8
else:
tabFmt = "&emsp;"
repDict = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
"\t" : tabFmt,
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
}
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doConvert(self):
htmlTags = {
self.FMT_B_B : "<strong>",
self.FMT_B_E : "</strong>",
self.FMT_I_B : "<em>",
self.FMT_I_E : "</em>",
self.FMT_U_B : "<u>",
self.FMT_U_E : "</u>",
}
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
aStyle = []
if tAlign == self.A_CENTRE:
aStyle.append("text-align: center;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
else:
hStyle = ""
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "".join(thisPar)
self.theResult += "<p%s>%s</p>\n" % (hStyle,tTemp.rstrip())
thisPar = []
elif tType == self.T_HEAD1:
self.theResult += "<h1%s>%s</h1>\n" % (hStyle,tText)
elif tType == self.T_HEAD2:
self.theResult += "<h2%s>%s</h2>\n" % (hStyle,tText)
elif tType == self.T_HEAD3:
self.theResult += "<h3%s>%s</h3>\n" % (hStyle,tText)
elif tType == self.T_HEAD4:
self.theResult += "<h4%s>%s</h4>\n" % (hStyle,tText)
elif tType == self.T_SEP:
self.theResult += "<p%s>%s</p>\n" % (hStyle,tText)
elif tType == self.T_SKIP:
self.theResult += "<p>&nbsp;</p>\n"
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"<br/>")
else:
thisPar.append(tTemp.rstrip()+" ")
elif tType == self.T_COMMENT and self.doComments:
self.theResult += self._formatComments(tText)
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += self._formatTags(tText)
return
##
# Internal Functions
##
def _formatTags(self, tText):
if not self.forPreview:
return "<pre>@%s</pre>\n" % tText
tText = "@"+tText
isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
return "<div>%s</div>" % retText
def _formatComments(self, tText):
if not self.forPreview:
return "<div class='comment'>%s</div>\n" % tText
return "<p class='comment'>%s</p>\n" % tText
# END Class ToHtml
-155
View File
@@ -1,155 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter LaTeX Converter
novelWriter LaTeX Converter
===============================
Extends the Tokenizer class to write LaTeX
File History:
Created: 2019-10-24 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import codecs
import re
import nw
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
class ToLaTeX(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.texCodecFail = False
return
def doPostProcessing(self):
"""The latexcodec misses dashes and non-breaking spaces, so we
do those here.
"""
repDict = {
nwUnicode.U_ENDASH : "--",
nwUnicode.U_EMDASH : "---",
nwUnicode.U_NBSP : "~",
}
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theResult = xRep.sub(lambda x: repDict[x.group(0)], self.theResult)
return
def doConvert(self):
texTags = {
self.FMT_B_B : r"\textbf{",
self.FMT_B_E : r"}",
self.FMT_I_B : r"\textit{",
self.FMT_I_E : r"}",
self.FMT_U_B : r"\underline{",
self.FMT_U_E : r"}",
}
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
begText = ""
endText = "\n"
if tAlign == self.A_CENTRE:
begText = "\\begin{center}\n"
endText = "\\end{center}\n\n"
# First check if we have a comment or plain text, as they
# need some extra replacing before we proceed to wrapping
# and final formatting.
if tType == self.T_COMMENT:
tText = "%% %s" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+texTags[xFmt]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# Then the text can receive final formatting before we
# append it to the results. We also store text lines in a
# buffer and merge them only when we find an empty line
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
self.theResult += begText
for tTemp in thisPar:
self.theResult += "%s\n" % tTemp
self.theResult += endText
thisPar = []
elif tType == self.T_HEAD1:
self.theResult += begText
self.theResult += "{\\Huge %s}\n" % self._escapeUnicode(tText)
self.theResult += endText
elif tType == self.T_HEAD2:
self.theResult += "\\chapter*{%s}\n\n" % self._escapeUnicode(tText)
elif tType == self.T_HEAD3:
self.theResult += "\\section*{%s}\n\n" % self._escapeUnicode(tText)
elif tType == self.T_HEAD4:
self.theResult += "\\subsection*{%s}\n\n" % self._escapeUnicode(tText)
elif tType == self.T_SEP:
self.theResult += begText
self.theResult += "%s\n" % self._escapeUnicode(tText)
self.theResult += endText
elif tType == self.T_SKIP:
self.theResult += "\\bigskip\n"
self.theResult += "\\bigskip\n\n"
elif tType == self.T_TEXT:
if tText.endswith(" "):
thisPar.append(self._escapeUnicode(tText.rstrip())+"\\newline")
else:
thisPar.append(self._escapeUnicode(tText.rstrip()))
elif tType == self.T_PBREAK:
self.theResult += "\\newpage\n\n"
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += "%% @%s\n\n" % tText
return
def _escapeUnicode(self, theText):
try:
import latexcodec
return codecs.encode(theText, "ulatex+utf8")
except:
self.texCodecFail = True
return theText
# END Class ToLaTeX
-136
View File
@@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Markdown Text Converter
novelWriter Markdown Text Converter
=======================================
Extends the Tokenizer class to write Markdown
File History:
Created: 2019-10-19 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import textwrap
import logging
import re
import nw
from nw.convert.tokenizer import Tokenizer
logger = logging.getLogger(__name__)
class ToMarkdown(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
return
def doConvert(self):
mdTags = {
self.FMT_B_B : "**",
self.FMT_B_E : "**",
self.FMT_I_B : "_",
self.FMT_I_E : "_",
self.FMT_U_B : "__",
self.FMT_U_E : "__",
}
if self.wordWrap > 0:
tWrap = textwrap.TextWrapper(
width = self.wordWrap,
initial_indent = "",
subsequent_indent = "",
expand_tabs = True,
replace_whitespace = True,
fix_sentence_endings = False,
break_long_words = True,
drop_whitespace = True,
break_on_hyphens = True,
tabsize = 8,
max_lines = None
)
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
# First check if we have a comment or plain text, as they
# need some extra replacing before we proceed to wrapping
# and final formatting.
if tType == self.T_COMMENT:
tText = " %s" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+mdTags[xFmt]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# The text can now be word wrapped, if we have requested
# this and it's needed.
if self.wordWrap > 0 and tLen > self.wordWrap:
if tType == self.T_COMMENT:
tText = textwrap.fill(
tText.strip(),initial_indent=" ",subsequent_indent=" "
)
else:
tText = tWrap.fill(tText)
# Then the text can receive final formatting before we
# append it to the results. We also store text lines in a
# buffer and merge them only when we find an empty line,
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "\n".join(thisPar)
self.theResult += "%s\n\n" % tTemp.rstrip()
thisPar = []
elif tType == self.T_HEAD1:
self.theResult += "# %s\n\n" % tText
elif tType == self.T_HEAD2:
self.theResult += "## %s\n\n" % tText
elif tType == self.T_HEAD3:
self.theResult += "### %s\n\n" % tText
elif tType == self.T_HEAD4:
self.theResult += "#### %s\n\n" % tText
elif tType == self.T_SEP:
self.theResult += "%s\n\n" % tText
elif tType == self.T_SKIP:
self.theResult += "\n\n\n"
elif tType == self.T_TEXT:
thisPar.append(tText)
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += "%s\n\n" % tText
return
# END Class ToMarkdown
-154
View File
@@ -1,154 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Plain Text Converter
novelWriter Plain Text Converter
====================================
Extends the Tokenizer class to convert to plain text
File History:
Created: 2019-10-26 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import textwrap
import logging
import re
import nw
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
class ToText(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
return
def doAutoReplace(self):
Tokenizer.doAutoReplace(self)
repDict = {
"\t" : " ",
nwUnicode.U_NBSP : " ",
}
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doConvert(self):
"""Converts the tokenized text into plain text.
"""
if self.wordWrap > 0:
tWrap = textwrap.TextWrapper(
width = self.wordWrap,
initial_indent = "",
subsequent_indent = "",
expand_tabs = True,
replace_whitespace = True,
fix_sentence_endings = False,
break_long_words = True,
drop_whitespace = True,
break_on_hyphens = True,
tabsize = 8,
max_lines = None
)
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
# First check if we have a comment or plain text, as they
# need some extra replacing before we proceed to wrapping
# and final formatting.
if tType == self.T_COMMENT:
tText = "[%s]" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# The text can now be word wrapped, if we have requested
# this and it's needed.
if tAlign == self.A_CENTRE:
if self.wordWrap > 0:
if tLen > self.wordWrap:
aText = tWrap.wrap(tText)
for n in range(len(aText)):
aText[n] = self._centreText(aText[n],self.wordWrap)
tText = "\n".join(aText)
else:
tText = self._centreText(tText,self.wordWrap)
else:
if self.wordWrap > 0 and tLen > self.wordWrap:
tText = tWrap.fill(tText)
# Then the text can receive final formatting before we
# append it to the results. We also store text lines in a
# buffer and merge them only when we find an empty line,
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "\n".join(thisPar)
self.theResult += "%s\n\n" % tTemp.rstrip()
thisPar = []
elif tType == self.T_HEAD1:
uLine = "="*min(tLen,self.wordWrap)
if tAlign == self.A_CENTRE:
uLine = self._centreText(uLine,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD2:
uLine = "~"*min(tLen,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD3:
uLine = "-"*min(tLen,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD4:
self.theResult += "%s\n\n" % tText
elif tType == self.T_SEP:
if self.wordWrap > 0 and tLen < self.wordWrap:
tText = self._centreText(tText,self.wordWrap)
self.theResult += "%s\n\n" % tText
elif tType == self.T_SKIP:
self.theResult += "\n\n\n"
elif tType == self.T_TEXT:
thisPar.append(tText)
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += "%s\n\n" % tText
return
# END Class ToText
-345
View File
@@ -1,345 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Text Tokenizer
novelWriter Text Tokenizer
==============================
Splits a piece of nW markdown text into its elements
File History:
Created: 2019-05-05 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from operator import itemgetter
from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord
from nw.constants import nwItemLayout
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
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
A_LEFT = 1 # Left aligned
A_RIGHT = 2 # Right aligned
A_CENTRE = 3 # Centred
A_JUSTIFY = 4 # Justified
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theText = None
self.theHandle = None
self.theItem = None
self.theTokens = None
self.theResult = None
self.wordWrap = 0
self.doComments = False
self.doKeywords = False
self.fmtTitle = "%title%"
self.fmtChapter = "%title%"
self.fmtUnNum = "%title%"
self.fmtScene = "%title%"
self.fmtSection = "%title%"
self.hideScene = False
self.hideSection = False
self.numChapter = 0
self.firstScene = False
return
##
# Setters
##
def setComments(self, doComments):
self.doComments = doComments
return
def setKeywords(self, doKeywords):
self.doKeywords = doKeywords
return
def setWordWrap(self, wordWrap):
if wordWrap >= 0:
self.wordWrap = wordWrap
else:
self.wordWrap = 0
return
def setTitleFormat(self, fmtTitle):
self.fmtTitle = fmtTitle
return
def setChapterFormat(self, fmtChapter):
self.fmtChapter = fmtChapter
return
def setUnNumberedFormat(self, fmtUnNum):
self.fmtUnNum = fmtUnNum
return
def setSceneFormat(self, fmtScene, hideScene):
self.fmtScene = fmtScene
self.hideScene = hideScene
return
def setSectionFormat(self, fmtSection, hideSection):
self.fmtSection = fmtSection
self.hideSection = hideSection
return
##
# Class Methods
##
def setText(self, theHandle, theText=None):
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if theText is not None:
# If the text is set, just use that
self.theText = theText
else:
# Otherwise, load it from file
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
return
def doAutoReplace(self):
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
repDict["<%s>" % aKey] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doPostProcessing(self):
return
def tokenizeText(self):
"""Scan the text for either lines starting with specific
characters that indicate headers, comments, commands etc, or
just contains plain text. in the case of plain text, apply the
same RegExes that the syntax highlighter uses and save the
locations of these formatting tags into the token array.
"""
# RegExes for adding formatting tags within text lines
# Keep in sync with the DocHighlighter class
rxFormats = [(
QRegularExpression(r"(?<![\w|\\])([\*]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_B_B, None, self.FMT_B_E]
),(
QRegularExpression(r"(?<![\w|_|\\])([_])(?!\s|\1)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_I_B, None, self.FMT_I_E]
),(
QRegularExpression(r"(?<![\w|\\])([_]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_U_B, None, self.FMT_U_E]
)]
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))
elif aLine[0] == "%":
self.theTokens.append((self.T_COMMENT,aLine[1:].strip(),None,self.A_LEFT))
elif aLine[0] == "@":
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))
elif aLine[:3] == "## ":
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))
elif aLine[:5] == "#### ":
self.theTokens.append((self.T_HEAD4,aLine[5:].strip(),None,self.A_LEFT))
else:
# Otherwise we use RegEx to find formatting tags within a line of text
fmtPos = []
for theRX, theKeys in rxFormats:
rxThis = theRX.globalMatch(aLine, 0)
while rxThis.hasNext():
rxMatch = rxThis.next()
for n in range(1,len(theKeys)):
if theKeys[n] is not None:
xPos = rxMatch.capturedStart(n)
xLen = rxMatch.capturedLength(n)
fmtPos.append([xPos,xLen,theKeys[n]])
# 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))
# Always add an empty line at the end
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
return
def doHeaders(self):
isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
isBook = self.theItem.itemLayout == nwItemLayout.BOOK
isPage = self.theItem.itemLayout == nwItemLayout.PAGE
isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
isScene = self.theItem.itemLayout == nwItemLayout.SCENE
isNote = self.theItem.itemLayout == nwItemLayout.NOTE
# No special header formatting for notes and no-layout files
if isNone: return
if isNote: return
# For novel files, we need to handle chapter numbering and scene
# breaks
if isBook or isUnNum or isChap or isScene:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
if tType == self.T_TEXT:
self.firstScene = False
elif tType == self.T_HEAD2:
if not isUnNum:
self.numChapter += 1
tText = self._doFormatChapter(tText,isUnNum)
self.theTokens[n] = (tType,tText,None,self.A_LEFT)
self.firstScene = True
elif tType == self.T_HEAD3:
tTemp = self._doFormatScene(tText)
if tTemp == "" and self.hideScene:
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)
else:
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)
else:
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE)
else:
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
self.firstScene = False
elif tType == self.T_HEAD4:
tTemp = self._doFormatSection(tText)
if tTemp == "" and self.hideSection:
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)
elif tTemp == self.fmtSection:
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE)
else:
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
if isTitle or isPart:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE)
self.theTokens.append((self.T_PBREAK,"",None,self.A_LEFT))
return
##
# Internal Functions
##
def _doFormatTitle(self, theText):
theTitle = self.fmtTitle
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _doFormatChapter(self, theText, noNum):
if noNum:
theTitle = self.fmtUnNum
theTitle = theTitle.replace("%title%", theText)
else:
theTitle = self.fmtChapter
theTitle = theTitle.replace("%title%", theText)
theTitle = theTitle.replace("%num%", str(self.numChapter))
theTitle = theTitle.replace("%numword%", numberToWord(self.numChapter,"en"))
return theTitle
def _doFormatScene(self, theText):
theTitle = self.fmtScene
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _doFormatSection(self, theText):
theTitle = self.fmtSection
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _centreText(self, theText, theWidth):
tLen = len(theText)
if tLen < theWidth:
return " "*int((theWidth-tLen)/2) + theText
return theText
# END Class Tokenizer
+4
View File
@@ -6,6 +6,8 @@ from nw.core.project import NWProject
from nw.core.spellcheck import NWSpellCheck
from nw.core.spellcheck import NWSpellEnchant
from nw.core.spellcheck import NWSpellSimple
from nw.core.tokenizer import Tokenizer
from nw.core.tohtml import ToHtml
from nw.core.tools import countWords
from nw.core.tools import projectMaintenance
from nw.core.tools import numberToWord
@@ -17,6 +19,8 @@ __all__ = [
"NWSpellCheck",
"NWSpellEnchant",
"NWSpellSimple",
"Tokenizer",
"ToHtml",
"countWords",
"projectMaintenance",
"numberToWord",
+55 -10
View File
@@ -46,7 +46,7 @@ from nw.core.tools import projectMaintenance
from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
logger = logging.getLogger(__name__)
@@ -82,10 +82,9 @@ class NWProject():
self.bookTitle = "" # The final title; should only be used for exports
self.bookAuthors = [] # A list of book authors
# Various
self.autoReplace = {} # Text to auto-replace on exports
# Project Settings
self.autoReplace = {} # Text to auto-replace on exports
self.titleFormat = {} # The formatting of titles for exports
self.spellCheck = False # Controls the spellcheck-as-you-type feature
self.autoOutline = True # If true, the Project Outline is updated automatically
self.statusItems = None # Novel file progress status values
@@ -202,6 +201,16 @@ class NWProject():
self.bookTitle = ""
self.bookAuthors = []
self.autoReplace = {}
self.titleFormat = {
"title" : r"%title%",
"chapter" : r"Chapter %num%\\%title%",
"unnumbered" : r"%title%",
"scene" : r"* * *",
"section" : r"",
"withSynopsis" : False,
"withComments" : False,
"withKeywords" : False,
}
self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus()
@@ -364,6 +373,11 @@ class NWProject():
elif xItem.tag == "autoReplace":
for xEntry in xItem:
self.autoReplace[xEntry.tag] = checkString(xEntry.text, None, False)
elif xItem.tag == "titleFormat":
titleFormat = self.titleFormat.copy()
for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
self.setTitleFormat(titleFormat)
elif xChild.tag == "content":
logger.debug("Found project content")
self.projTree.unpackXML(xChild)
@@ -435,10 +449,16 @@ class NWProject():
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
xAutoRep = etree.SubElement(xSettings, "autoReplace")
for aKey, aValue in self.autoReplace.items():
if len(aKey) > 0:
self._packProjectValue(xAutoRep,aKey,aValue)
self._packProjectValue(xAutoRep, aKey, aValue)
xTitleFmt = etree.SubElement(xSettings, "titleFormat")
for aKey, aValue in self.titleFormat.items():
if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue)
xStatus = etree.SubElement(xSettings,"status")
self.statusItems.packEntries(xStatus)
@@ -527,11 +547,7 @@ class NWProject():
), nwAlert.WARN)
return False
cleanName = ""
for c in self.projName.strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
cleanName = self.getFileSafeProjectName()
baseDir = path.join(self.mainConf.backupPath, cleanName)
if not path.isdir(baseDir):
try:
@@ -713,6 +729,16 @@ class NWProject():
self.autoReplace = autoReplace
return
def setTitleFormat(self, titleFormat):
"""Set the formatting of titles in the project.
"""
for valKey, valEntry in titleFormat.items():
if valKey in ("title","chapter","unnumbered","scene","section"):
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
elif valKey in ("withSynopsis","withComments","withKeywords"):
self.titleFormat[valKey] = checkBool(valEntry, False, False)
return
def setProjectChanged(self, bValue):
"""Toggle the project changed flag, and propagate the
information to the GUI statusbar.
@@ -728,6 +754,15 @@ class NWProject():
# Getters
##
def getFileSafeProjectName(self):
"""Returns a filename safe version of the project name.
"""
cleanName = ""
for c in self.projName.strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
def getSessionWordCount(self):
"""Returns the number of words added or removed this session.
"""
@@ -1306,6 +1341,7 @@ class NWItem():
self.itemLayout = nwItemLayout.NO_LAYOUT
self.itemStatus = None
self.isExpanded = False
self.isExported = True
# Document Meta Data
self.charCount = 0
@@ -1333,6 +1369,7 @@ class NWItem():
xSub = self._subPack(xPack,"status", text=str(self.itemStatus))
xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
if self.itemType == nwItemType.FILE:
xSub = self._subPack(xPack,"exported", text=str(self.isExported))
xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name))
xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False)
xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False)
@@ -1365,6 +1402,7 @@ class NWItem():
"layout" : self.setLayout,
"status" : self.setStatus,
"expanded" : self.setExpanded,
"exported" : self.setExported,
"charCount" : self.setCharCount,
"wordCount" : self.setWordCount,
"paraCount" : self.setParaCount,
@@ -1465,6 +1503,13 @@ class NWItem():
self.isExpanded = expState == True
return
def setExported(self, expState):
if isinstance(expState, str):
self.isExported = expState == str(True)
else:
self.isExported = expState == True
return
##
# Set Document Meta Data
##
+266
View File
@@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
"""novelWriter HTML Text Converter
novelWriter HTML Text Converter
===================================
Extends the Tokenizer class to write HTML
File History:
Created: 2019-05-07 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from nw.core.tokenizer import Tokenizer
from nw.constants import nwUnicode, nwLabels, nwKeyWords
logger = logging.getLogger(__name__)
class ToHtml(Tokenizer):
M_PREVIEW = 0 # Tweak output for the DocViewer
M_EXPORT = 1 # Tweak output for saving to HTML or printing
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.genMode = self.M_EXPORT
self.repDict = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
"\t" : "&emsp;",
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
}
return
##
# Setters
##
def setPreview(self, forPreview, doComments):
"""If we're using this class to generate markdown preview, we
need to make a few changes to formatting, which is managed by
these flags.
"""
if forPreview:
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.repDict["\t"] = "&nbsp;"*8
return
##
# Class Methods
##
def doAutoReplace(self):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
Tokenizer.doAutoReplace(self)
xRep = re.compile("|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: self.repDict[x.group(0)], self.theText)
return
def doPostProcessing(self):
"""Reverse the html entities replacement on the markdown text.
Otherwise, all the &something; bits will also be in there.
"""
if self.genMode == self.M_PREVIEW:
# Doesn't matter for preview as we don't use the markdown
return
revDict = dict(map(reversed, self.repDict.items()))
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
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
"""
htmlTags = {
self.FMT_B_B : "<strong>",
self.FMT_B_E : "</strong>",
self.FMT_I_B : "<em>",
self.FMT_I_E : "</em>",
self.FMT_U_B : "<u>",
self.FMT_U_E : "</u>",
}
self.theResult = ""
thisPar = []
parStyle = ""
tmpResult = []
for tType, tText, tFormat, tStyle in self.theTokens:
# Styles
aStyle = []
if tStyle is not None:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
if tStyle & self.A_RIGHT:
aStyle.append("text-align: right;")
if tStyle & self.A_CENTRE:
aStyle.append("text-align: center;")
if 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_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:
aStyle.append("page-break-before: avoid;")
if tStyle & self.A_PBA:
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:
aStyle.append("page-break-after: avoid;")
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)
tmpResult.append("<p%s>%s</p>\n" % (parStyle, tTemp.rstrip()))
thisPar = []
parStyle = ""
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1%s>%s</h1>\n" % (hStyle, tHead))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h2%s>%s</h2>\n" % (hStyle, tHead))
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h3%s>%s</h3>\n" % (hStyle, tHead))
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h4%s>%s</h4>\n" % (hStyle, tHead))
elif tType == self.T_SEP:
tmpResult.append("<p%s>%s</p>\n" % (hStyle, tText))
elif tType == self.T_SKIP:
tmpResult.append("<p%s>&nbsp;</p>\n" % hStyle)
elif tType == self.T_TEXT:
tTemp = tText
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"<br/>")
else:
thisPar.append(tTemp.rstrip()+" ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText))
self.theResult = "".join(tmpResult)
tmpResult = []
return
##
# Internal Functions
##
def _formatSynopsis(self, tText):
"""Apply HTML formatting to synopsis.
"""
if self.genMode == self.M_EXPORT:
return "<p class='synopsis'><strong>Synopsis: </strong>%s</p>\n" % tText
else:
return "<p class='comment'>%s</p>\n" % tText
def _formatComments(self, tText):
"""Apply HTML formatting to comments.
"""
if self.genMode == self.M_EXPORT:
return "<p class='comment'><strong>Comment: </strong>%s</p>\n" % tText
else:
return "<p class='comment'>%s</p>\n" % tText
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
"""
tText = "@"+tText
isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
else:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'/>%s" % (
theBits[1], theBits[1]
)
else:
for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % (
tTag, tTag
))
retText += ", ".join(refTags)
return "<div>%s</div>" % retText
# END Class ToHtml
+533
View File
@@ -0,0 +1,533 @@
# -*- coding: utf-8 -*-
"""novelWriter Text Tokenizer
novelWriter Text Tokenizer
==============================
Splits a piece of nW markdown text into its elements
File History:
Created: 2019-05-05 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from operator import itemgetter
from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord
from nw.constants import nwItemLayout
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
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
A_LEFT = 1 # Left aligned
A_RIGHT = 2 # Right aligned
A_CENTRE = 4 # Centred
A_JUSTIFY = 8 # Justified
A_PBB = 16 # Page break before
A_PBB_L = 32 # Page break before, left
A_PBB_R = 64 # Page break before, right
A_PBB_AV = 128 # Page break, avoid
A_PBA = 256 # Page break after
A_PBA_L = 512 # Page break after, left
A_PBA_R = 1024 # Page break after, right
A_PBA_AV = 2048 # Page break, avoid
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
# Data Variables
self.theText = None # 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
# 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
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
self.fmtScene = "%title%" # Formatting for scenes
self.fmtSection = "%title%" # Formatting for sections
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
self.numAbsScene = 0 # Counter for scene number within novel
self.firstScene = False # Flag to indicate that the first scene of the chapter
# This File
self.isNone = False
self.isTitle = False
self.isBook = False
self.isPage = False
self.isPart = False
self.isUnNum = False
self.isChap = False
self.isScene = False
self.isNote = False
self.isNovel = False
return
def clearData(self):
"""Clear the data arrays and variables, but not settings, so the class
can be reused for multiple documents.
"""
self.theText = None
self.theHandle = None
self.theItem = None
self.theTokens = None
self.theResult = None
self.theMarkdown = None
self.numChapter = 0
self.firstScene = False
self.isNone = False
self.isTitle = False
self.isBook = False
self.isPage = False
self.isPart = False
self.isUnNum = False
self.isChap = False
self.isScene = False
self.isNote = False
self.isNovel = False
return
##
# Setters
##
def setTitleFormat(self, fmtTitle):
self.fmtTitle = fmtTitle
return
def setChapterFormat(self, fmtChapter):
self.fmtChapter = fmtChapter
return
def setUnNumberedFormat(self, fmtUnNum):
self.fmtUnNum = fmtUnNum
return
def setSceneFormat(self, fmtScene, hideScene):
self.fmtScene = fmtScene
self.hideScene = hideScene
return
def setSectionFormat(self, fmtSection, hideSection):
self.fmtSection = fmtSection
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
##
def setText(self, theHandle, theText=None):
"""Set the text for the tokenizer from a handle. If theText is
not set, load it from the file.
"""
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if theText is not None:
# If the text is set, just use that
self.theText = theText
else:
# Otherwise, load it from file
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
self.isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
self.isBook = self.theItem.itemLayout == nwItemLayout.BOOK
self.isPage = self.theItem.itemLayout == nwItemLayout.PAGE
self.isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
self.isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
self.isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
self.isScene = self.theItem.itemLayout == nwItemLayout.SCENE
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
return
def getResult(self):
"""Return the result from the conversion.
"""
return 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.
"""
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
repDict["<%s>" % aKey] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doPostProcessing(self):
"""Do some postprocessing. Overloaded by subclasses.
"""
return
def tokenizeText(self):
"""Scan the text for either lines starting with specific
characters that indicate headers, comments, commands etc, or
just contains plain text. in the case of plain text, apply the
same RegExes that the syntax highlighter uses and save the
locations of these formatting tags into the token array.
The format of the token list is an entry with a four-tuple for
each line in the file. The tuple is as follows:
1: The type of the block, self.T_*
2: The text content of the block, without leading tags
3: The internal formatting map of the text, self.FMT_*
4: The style of the block, self.A_*
"""
# RegExes for adding formatting tags within text lines
# Keep in sync with the DocHighlighter class
rxFormats = [(
QRegularExpression(r"(?<![\w|\\])([\*]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_B_B, None, self.FMT_B_E]
),(
QRegularExpression(r"(?<![\w|_|\\])([_])(?!\s|\1)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_I_B, None, self.FMT_I_E]
),(
QRegularExpression(r"(?<![\w|\\])([_]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[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.theMarkdown = ""
tmpMarkdown = []
for aLine in self.theText.splitlines():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
self.T_EMPTY, "", None, None
))
tmpMarkdown.append("\n")
elif aLine[0] == "%":
cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"):
self.theTokens.append((
self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign
))
if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self.T_COMMENT, aLine[1:].strip(), None, defAlign
))
if self.doComments:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self.T_KEYWORD, aLine[1:].strip(), None, self.A_LEFT
))
if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT | self.A_PBB
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT | self.A_PBA_AV
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT | self.A_PBA_AV
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT | self.A_PBA_AV
))
tmpMarkdown.append("%s\n" % aLine)
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:
rxThis = theRX.globalMatch(aLine, 0)
while rxThis.hasNext():
rxMatch = rxThis.next()
for n in range(1,len(theKeys)):
if theKeys[n] is not None:
xPos = rxMatch.capturedStart(n)
xLen = rxMatch.capturedLength(n)
fmtPos.append([xPos,xLen,theKeys[n]])
# 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, defAlign
))
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
self.T_EMPTY, "", None, None
))
tmpMarkdown.append("\n")
self.theMarkdown = "".join(tmpMarkdown)
tmpMarkdown = []
return
def doHeaders(self):
"""Apply formatting to the text headers according to document
layout and user settings.
"""
# No special header formatting for notes and no-layout files
if self.isNone or self.isNote:
return
# For novel files, we need to handle chapter numbering and scene
# breaks
if self.isNovel:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
# In case we see text before a scene, we reset the flag
if tType == self.T_TEXT:
self.firstScene = False
elif tType == self.T_HEAD1:
# Main Title
# ==========
tText = self._formatHeading(self.fmtTitle, tText)
self.theTokens[n] = (
tType, tText, None, self.A_LEFT | self.A_PBB_R
)
elif tType == self.T_HEAD2:
# Novel Chapter
# =============
# Numbered or Unnumbered
if self.isUnNum:
tText = self._formatHeading(self.fmtUnNum, tText)
else:
self.numChapter += 1
tText = self._formatHeading(self.fmtChapter, tText)
# Format the chapter header
self.theTokens[n] = (
tType, tText, None, self.A_LEFT | self.A_PBB_R
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
elif tType == self.T_HEAD3:
# Novel Scene
# ===========
self.numChScene += 1
self.numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tText)
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
else:
self.theTokens[n] = (
self.T_SKIP, "", None, None
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
else:
self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tTemp, None, self.A_LEFT | self.A_PBA_AV
)
# Definitely no longer the first scene
self.firstScene = False
elif tType == self.T_HEAD4:
# Novel Section
# =============
tTemp = self._formatHeading(self.fmtSection, tText)
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
self.T_SKIP, "", None, None
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tTemp, None, self.A_LEFT | self.A_PBA_AV
)
# For title page and partitions, we need to centre all text.
# For partition, we also add a page break before, and for
# both types we always add a page break after the content.
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
if self.isTitle:
self.theTokens[n] = (
tType, tText, tFormat, self.A_CENTRE
)
# Add a page break after the last entry
n = len(self.theTokens) - 1
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA
)
return
##
# Internal Functions
##
def _formatHeading(self, theTitle, theText):
"""Replaces the %keyword% strings.
"""
theTitle = theTitle.replace(r"%title%", theText)
theTitle = theTitle.replace(r"%chnum%", str(self.numChapter))
theTitle = theTitle.replace(r"%scnum%", str(self.numChScene))
theTitle = theTitle.replace(r"%scabsnum%", str(self.numAbsScene))
theTitle = theTitle.replace(r"%chnumword%", numberToWord(self.numChapter,"en"))
return theTitle
# END Class Tokenizer
+8 -8
View File
@@ -1,20 +1,20 @@
# -*- coding: utf-8 -*-
# Qt Additions
from nw.gui.additions.qconfiglayout import QConfigLayout
from nw.gui.additions.qswitch import QSwitch
# Main Window Elements
from nw.gui.build import GuiBuildNovel
from nw.gui.icons import GuiIcons
from nw.gui.mainmenu import GuiMainMenu
from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiTheme
# Qt Additions
from nw.gui.additions.qconfiglayout import QConfigLayout
from nw.gui.additions.qswitch import QSwitch
# Dialogs
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
@@ -37,16 +37,16 @@ from nw.gui.tools.optionstate import OptionState
from nw.gui.tools.wordcounter import WordCounter
__all__ = [
"QConfigLayout",
"QSwitch",
"GuiBuildNovel",
"GuiIcons",
"GuiMainMenu",
"GuiMainStatus",
"GuiTheme",
"QConfigLayout",
"QSwitch",
"GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
"GuiProjectLoad",
+691
View File
@@ -0,0 +1,691 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Build Novel
novelWriter GUI Build Novel
===============================
Class holding the build novel window
File History:
Created: 2020-05-09 [0.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from time import time
from PyQt5.QtCore import Qt, QByteArray
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
QTextOption, QPalette, QColor, QTextDocumentWriter
)
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFileDialog
)
from nw.gui.additions import QSwitch
from nw.core import ToHtml
from nw.constants import (
nwConst, nwFiles, nwAlert, nwItemType, nwItemLayout, nwItemClass
)
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
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiBuildNovel ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theTheme = theParent.theTheme
self.optState = self.theProject.optState
self.htmlText = [] # List of html document
self.nwdText = [] # List of markdown documents
self.textLayout = [] # List of nwItemLayout entries
self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(800)
self.setMinimumHeight(800)
self.resize(
self.optState.getInt("GuiBuildNovel", "winWidth", 800),
self.optState.getInt("GuiBuildNovel", "winHeight", 800)
)
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.toolsBox = QVBoxLayout()
self.docView = GuiBuildNovelDocView(self, self.theProject)
# Title Formats
# =============
self.titleGroup = QGroupBox("Novel Title Formats", self)
self.titleForm = QGridLayout(self)
self.titleGroup.setLayout(self.titleForm)
self.fmtTitle = QLineEdit()
self.fmtTitle.setMaxLength(200)
self.fmtTitle.setFixedWidth(200)
self.fmtTitle.setText(self.theProject.titleFormat["title"])
self.fmtChapter = QLineEdit()
self.fmtChapter.setMaxLength(200)
self.fmtChapter.setFixedWidth(200)
self.fmtChapter.setText(self.theProject.titleFormat["chapter"])
self.fmtUnnumbered = QLineEdit()
self.fmtUnnumbered.setMaxLength(200)
self.fmtUnnumbered.setFixedWidth(200)
self.fmtUnnumbered.setText(self.theProject.titleFormat["unnumbered"])
self.fmtScene = QLineEdit()
self.fmtScene.setMaxLength(200)
self.fmtScene.setFixedWidth(200)
self.fmtScene.setText(self.theProject.titleFormat["scene"])
self.fmtSection = QLineEdit()
self.fmtSection.setMaxLength(200)
self.fmtSection.setFixedWidth(200)
self.fmtSection.setText(self.theProject.titleFormat["section"])
self.titleForm.addWidget(QLabel("Title"), 0, 0)
self.titleForm.addWidget(self.fmtTitle, 0, 1)
self.titleForm.addWidget(QLabel("Chapter"), 1, 0)
self.titleForm.addWidget(self.fmtChapter, 1, 1)
self.titleForm.addWidget(QLabel("Unnumbered"), 2, 0)
self.titleForm.addWidget(self.fmtUnnumbered, 2, 1)
self.titleForm.addWidget(QLabel("Scene"), 3, 0)
self.titleForm.addWidget(self.fmtScene, 3, 1)
self.titleForm.addWidget(QLabel("Section"), 4, 0)
self.titleForm.addWidget(self.fmtSection, 4, 1)
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)
# Include Switches
# ================
self.includeGroup = QGroupBox("Include Non-Text Elements", self)
self.includeForm = QGridLayout(self)
self.includeGroup.setLayout(self.includeForm)
self.includeSynopsis = QSwitch()
self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"])
self.includeComments = QSwitch()
self.includeComments.setChecked(self.theProject.titleFormat["withComments"])
self.includeKeywords = QSwitch()
self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"])
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(self.includeComments, 1, 1)
self.includeForm.addWidget(QLabel("Include keywords"), 2, 0)
self.includeForm.addWidget(self.includeKeywords, 2, 1)
self.includeForm.setColumnStretch(0, 1)
self.includeForm.setColumnStretch(1, 0)
# Additional Options
# ==================
self.addsGroup = QGroupBox("Additional Options", self)
self.addsForm = QGridLayout(self)
self.addsGroup.setLayout(self.addsForm)
self.novelFiles = QSwitch()
self.novelFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNovel", True))
self.noteFiles = QSwitch()
self.noteFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNotes", False))
self.ignoreFlag = QSwitch()
self.ignoreFlag.setChecked(self.optState.getBool("GuiBuildNovel", "ignoreFlag", False))
self.excludeBody = QSwitch()
self.excludeBody.setChecked(self.optState.getBool("GuiBuildNovel", "excludeBody", False))
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(self.noteFiles, 1, 1)
self.addsForm.addWidget(QLabel("Ignore export flag"), 2, 0)
self.addsForm.addWidget(self.ignoreFlag, 2, 1)
self.addsForm.addWidget(QLabel("Exclude body text"), 3, 0)
self.addsForm.addWidget(self.excludeBody, 3, 1)
self.addsForm.setColumnStretch(0, 1)
self.addsForm.setColumnStretch(1, 0)
# Build Button
# ============
self.buildProgress = QProgressBar()
self.genPreview = QPushButton("Generate Preview")
self.genPreview.clicked.connect(self._buildPreview)
# Action Buttons
# ==============
self.buttonForm = QGridLayout()
self.btnHelp = QPushButton("Help")
self.btnHelp.clicked.connect(self._showHelp)
self.btnPrint = QPushButton("Print")
self.btnPrint.clicked.connect(self._printDocument)
self.btnSave = QPushButton("Save As")
self.saveMenu = QMenu(self)
self.btnSave.setMenu(self.saveMenu)
self.saveODT = QAction("Open Document (.odt)")
self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT))
self.saveMenu.addAction(self.saveODT)
self.savePDF = QAction("Portable Document Format (.pdf)")
self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.saveMenu.addAction(self.savePDF)
self.saveHTM = QAction("%s HTML (.htm)" % nw.__package__)
self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM))
self.saveMenu.addAction(self.saveHTM)
if self.mainConf.verQtValue >= 51400:
self.saveMD = QAction("Markdown (.md)")
self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD))
self.saveMenu.addAction(self.saveMD)
self.saveNWD = QAction("novelWriter Markdown (.nwd)")
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD)
self.saveTXT = QAction("Plain Text (.txt)")
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT)
self.btnClose = QPushButton("Close")
self.btnClose.clicked.connect(self._doClose)
self.buttonForm.addWidget(self.btnHelp, 0, 0)
self.buttonForm.addWidget(self.btnPrint, 0, 1)
self.buttonForm.addWidget(self.btnSave, 1, 0)
self.buttonForm.addWidget(self.btnClose, 1, 1)
# Assemble GUI
# ============
self.toolsBox.addWidget(self.titleGroup)
self.toolsBox.addWidget(self.textGroup)
self.toolsBox.addWidget(self.includeGroup)
self.toolsBox.addWidget(self.addsGroup)
self.toolsBox.addStretch(1)
self.toolsBox.addWidget(self.buildProgress)
self.toolsBox.addWidget(self.genPreview)
self.toolsBox.addSpacing(8)
self.toolsBox.addLayout(self.buttonForm)
self.innerBox.addLayout(self.toolsBox)
self.innerBox.addWidget(self.docView)
self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox)
self.innerBox.setStretch(0, 0)
self.innerBox.setStretch(1, 1)
self.show()
logger.debug("GuiBuildNovel initialisation complete")
return
##
# Slots
##
def _buildPreview(self):
"""Build a preview of the project in the document viewer.
"""
# 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()
incSynopsis = self.includeSynopsis.isChecked()
incComments = self.includeComments.isChecked()
incKeywords = self.includeKeywords.isChecked()
novelFiles = self.novelFiles.isChecked()
noteFiles = self.noteFiles.isChecked()
ignoreFlag = self.ignoreFlag.isChecked()
excludeBody = self.excludeBody.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(not excludeBody)
makeHtml.setSynopsis(incSynopsis)
makeHtml.setComments(incComments)
makeHtml.setKeywords(incKeywords)
makeHtml.setJustify(justifyText)
self.buildProgress.setMaximum(len(self.theProject.projTree))
self.buildProgress.setValue(0)
tStart = time()
self.htmlText = []
self.nwdText = []
self.textLayout = []
for nItt, tItem in enumerate(self.theProject.projTree):
if 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())
self.textLayout.append(tItem.itemLayout)
# Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1)
tEnd = time()
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
# Load the preview document with the html data
self.docView.setHtml("".join(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.
"""
# FMT_PDF
byteFmt = QByteArray()
fileExt = ""
textFmt = ""
outTool = ""
# Create the settings
if theFormat == self.FMT_ODT:
byteFmt.append("odf")
fileExt = "odt"
textFmt = "Open Document"
outTool = "Qt"
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"
else:
return False
# Generate the file name
if fileExt:
cleanName = self.theProject.getFileSafeProjectName()
fileName = "%s.%s" % (cleanName, fileExt)
saveDir = self.mainConf.lastPath
savePath = path.join(saveDir, fileName)
if not path.isdir(saveDir):
saveDir = self.mainConf.homePath
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo[0]:
savePath = saveTo[0]
else:
return False
self.mainConf.setLastPath(savePath)
else:
return False
# Do the actual writing
if outTool == "Qt":
docWriter = QTextDocumentWriter()
docWriter.setFileName(savePath)
docWriter.setFormat(byteFmt)
if 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":
try:
with open(savePath, mode="w", encoding="utf8") as outFile:
if theFormat == self.FMT_HTM:
# Write novelWriter HTML data
outFile.write("<!DOCTYPE html>\n")
outFile.write("<html>\n")
outFile.write("<head>\n")
outFile.write("<meta charset='utf-8'>\n")
outFile.write("</head>\n")
outFile.write("<body>\n")
outFile.write("<article style='width: 800px; margin: 40px auto'>\n")
for aLine in self.htmlText:
outFile.write(aLine)
outFile.write("</article>\n")
outFile.write("</body>\n")
outFile.write("</html>\n")
elif theFormat == self.FMT_NWD:
# Write novelWriter markdown data
for aLine in self.nwdText:
outFile.write(aLine)
self.theParent.makeAlert(
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
except Exception as e:
self.theParent.makeAlert(
"Failed to write document in %s format to file: %s" % (
textFmt, str(e)
), nwAlert.ERROR
)
elif outTool == "QtPrint" and theFormat == self.FMT_PDF:
try:
thePrinter = QPrinter()
thePrinter.setOutputFormat(QPrinter.PdfFormat)
thePrinter.setOrientation(QPrinter.Portrait)
thePrinter.setDuplex(QPrinter.DuplexLongSide)
thePrinter.setFontEmbeddingEnabled(True)
thePrinter.setColorMode(QPrinter.Color)
thePrinter.setOutputFileName(savePath)
self.docView.qDocument.print(thePrinter)
self.theParent.makeAlert(
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
except Exception as e:
self.theParent.makeAlert(
"Failed to write document in %s format to file: %s" % (
textFmt, str(e)
), nwAlert.ERROR
)
else:
return False
return True
def _printDocument(self):
"""Open the print preview dialog.
"""
thePreview = QPrintPreviewDialog(self)
thePreview.paintRequested.connect(self._doPrintPreview)
thePreview.exec_()
return
def _doPrintPreview(self, thePrinter):
"""Connect the print preview painter to the document viewer.
"""
thePrinter.setOrientation(QPrinter.Portrait)
thePrinter.setOutputFormat(QPrinter.NativeFormat | QPrinter.PdfFormat)
self.docView.qDocument.print(thePrinter)
return
def _doClose(self):
"""Close button was clicked.
"""
self.close()
return
##
# Events
##
def closeEvent(self, theEvent):
"""Capture the user closing the window so we can save settings.
"""
self._saveSettings()
QDialog.closeEvent(self, theEvent)
return
##
# Internal Functions
##
def _saveSettings(self):
"""Save the various user settings.
"""
logger.debug("Saving GuiBuildNovel settings")
# Formatting
self.theProject.setTitleFormat({
"title" : self.fmtTitle.text().strip(),
"chapter" : self.fmtChapter.text().strip(),
"unnumbered" : self.fmtUnnumbered.text().strip(),
"scene" : self.fmtScene.text().strip(),
"section" : self.fmtSection.text().strip(),
"withSynopsis" : self.includeSynopsis.isChecked(),
"withComments" : self.includeComments.isChecked(),
"withKeywords" : self.includeKeywords.isChecked(),
})
# 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", "addNovel", self.novelFiles.isChecked())
self.optState.setValue("GuiBuildNovel", "addNotes", self.noteFiles.isChecked())
self.optState.setValue("GuiBuildNovel", "ignoreFlag", self.ignoreFlag.isChecked())
self.optState.setValue("GuiBuildNovel", "excludeBody", self.excludeBody.isChecked())
self.optState.saveSettings()
return
def _showHelp(self):
"""Generate a help text and show it in the document window.
"""
docName = "exportHelp_%s.htm" % self.mainConf.guiLang
docPath = path.join(self.mainConf.assetPath, "text", docName)
if path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.docView.setText(helpText)
else:
self.theParent.makeAlert(
"Could not open help text file for Build Project.", nwAlert.ERROR
)
return
# END Class GuiBuildNovel
class GuiBuildNovelDocView(QTextBrowser):
def __init__(self, theParent, theProject):
QTextBrowser.__init__(self, theParent)
logger.debug("Initialising GuiBuildNovelDocView ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.setMinimumWidth(400)
self.setOpenExternalLinks(False)
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
theOpt = QTextOption()
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
self.qDocument.setDefaultTextOption(theOpt)
docPalette = self.palette()
docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
docPalette.setColor(QPalette.Text, QColor( 0, 0, 0))
self.setPalette(docPalette)
self._makeStyleSheet()
self.show()
logger.debug("GuiBuildNovelDocView initialisation complete")
return
def setText(self, theText):
self.setHtml(theText)
return
##
# Internal Functions
##
def _makeStyleSheet(self):
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
# END Class GuiBuildNovelDocView
-2
View File
@@ -3,7 +3,6 @@
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
@@ -13,7 +12,6 @@ __all__ = [
"GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
"GuiProjectLoad",
+1
View File
@@ -59,6 +59,7 @@ class GuiConfigEditor(QDialog):
self.setWindowTitle("Preferences")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64))
self.outerBox.setSpacing(16)
self.tabGeneral = GuiConfigEditGeneralTab(self.theParent)
self.tabLayout = GuiConfigEditLayoutTab(self.theParent)
+2 -1
View File
@@ -52,10 +52,11 @@ class GuiDocMerge(QDialog):
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Merge Documents")
self.setLayout(self.outerBox)
self.setWindowTitle("Merge Documents")
self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(64,64))
self.outerBox.setSpacing(16)
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
+2 -1
View File
@@ -53,10 +53,11 @@ class GuiDocSplit(QDialog):
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Split Document")
self.setLayout(self.outerBox)
self.setWindowTitle("Split Document")
self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64))
self.outerBox.setSpacing(16)
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
-718
View File
@@ -1,718 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Export Tools
novelWriter GUI Export Tools
================================
Tool for exporting project files to other formats
File History:
Created: 2019-10-13 [0.2.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import time
import nw
from os import path
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QGridLayout,
QGroupBox, QCheckBox, QLabel, QComboBox, QLineEdit, QPushButton,
QFileDialog, QProgressBar, QSpinBox, QMessageBox
)
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
from nw.common import packageRefURL
from nw.constants import nwFiles, nwItemType, nwAlert
logger = logging.getLogger(__name__)
class GuiExport(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiExport ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = self.theProject.optState
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Export Project")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64))
self.tabMain = GuiExportMain(self.theParent, self.theProject)
self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject)
self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "Settings")
self.tabWidget.addTab(self.tabPandoc, "Pandoc")
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
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.addLayout(self.doExportForm)
self.rejected.connect(self._doClose)
self.show()
logger.debug("GuiExport initialisation complete")
return
##
# Buttons
##
def _doExport(self):
logger.verbose("GuiExport export button clicked")
wNovel = self.tabMain.expNovel.isChecked()
wNotes = self.tabMain.expNotes.isChecked()
eFormat = self.tabMain.outputFormat.currentData()
fixWidth = self.tabMain.fixedWidth.value()
wComments = self.tabMain.expComments.isChecked()
wKeywords = self.tabMain.expKeywords.isChecked()
chFormat = self.tabMain.chapterFormat.text()
unFormat = self.tabMain.unnumFormat.text()
scFormat = self.tabMain.sceneFormat.text()
seFormat = self.tabMain.sectionFormat.text()
saveTo = self.tabMain.exportPath.text()
hScene = self.tabMain.hideScene.isChecked()
hSection = self.tabMain.hideSection.isChecked()
pFormat = self.tabPandoc.outputFormat.currentData()
tFormat = GuiExportPandoc.FMT_VIA[pFormat]
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
exportDir = path.dirname(saveTo)
if not path.isdir(exportDir):
self.theParent.makeAlert("The export folder does not exist.",nwAlert.ERROR)
self.exportStatus.setText("Export failed ...")
return False
nItems = len(self.theProject.projTree)
if eFormat == GuiExportMain.FMT_PDOC:
nItems += int(0.2*nItems)
self.exportProgress.setMinimum(0)
self.exportProgress.setMaximum(nItems)
self.exportProgress.setValue(0)
if not wNovel and not wNotes:
self.exportStatus.setText("Nothing to export ...")
return False
outFile = None
if eFormat == GuiExportMain.FMT_TXT:
outFile = TextFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_MD:
outFile = MarkdownFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_HTML:
outFile = HtmlFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_TEX:
outFile = LaTeXFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_NWD:
outFile = ConcatFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_PDOC:
if tFormat == "html":
outFile = HtmlFile(self.theProject, self.theParent)
elif tFormat == "markdown":
outFile = MarkdownFile(self.theProject, self.theParent)
if outFile is None:
return False
if outFile.openFile(saveTo):
outFile.setComments(wComments)
outFile.setKeywords(wKeywords)
outFile.setExportNovel(wNovel)
outFile.setExportNotes(wNotes)
outFile.setWordWrap(fixWidth)
outFile.setChapterFormat(chFormat)
outFile.setUnNumberedFormat(unFormat)
outFile.setSceneFormat(scFormat, hScene)
outFile.setSectionFormat(seFormat, hSection)
else:
self.exportStatus.setText("Failed to open file for writing ...")
return False
time.sleep(0.5)
nDone = 0
for tItem in self.theProject.projTree:
self.exportProgress.setValue(nDone)
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(tItem.itemHandle)
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)
if eFormat == GuiExportMain.FMT_TEX:
# Check that encoding was successful
if outFile.texCodecFail:
self.theParent.makeAlert((
"Failed to escape unicode characters while writing LaTeX "
"file. The generated .tex file may not build properly. "
"Make sure the python package '{package:s}' is installed "
"and working."
).format(
package = packageRefURL("latexcodec")
), nwAlert.WARN)
if eFormat != GuiExportMain.FMT_PDOC:
return True
# If we've reached this point, we're also running Pandoc
if self._callPandoc(saveTo, tFormat, pFormat):
self.exportProgress.setValue(nItems)
self.exportStatus.setText("Pandoc conversion complete")
logger.verbose("Pandoc conversion complete")
else:
self.exportProgress.setValue(nItems)
self.exportStatus.setText("Pandoc conversion failed")
logger.verbose("Pandoc conversion failed")
return False
return True
def _callPandoc(self, inFile, inFmt, outFmt):
pFmt = {
GuiExportPandoc.FMT_ODT : "odt",
GuiExportPandoc.FMT_DOCX : "docx",
GuiExportPandoc.FMT_EPUB2 : "epub2",
GuiExportPandoc.FMT_EPUB3 : "epub3",
GuiExportPandoc.FMT_ZIM : "zimwiki",
}
try:
import pypandoc
except:
self.theParent.makeAlert((
"Could not load the '{package:s}' package. "
"Make sure it is installed, and try again."
).format(
package = packageRefURL("pypandoc")
), nwAlert.ERROR)
return False
outFile = path.splitext(inFile)[0]+GuiExportPandoc.FMT_EXT[outFmt]
fileName = path.basename(outFile)
if path.isfile(outFile) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(
self.theParent, "Overwrite",
("File '%s' already exists.<br>Do you want to overwrite it?" % fileName)
)
if msgRes != QMessageBox.Yes:
return False
try:
pypandoc.convert_file(
source_file = inFile,
format = inFmt,
outputfile = outFile,
to = pFmt[outFmt],
extra_args = (),
encoding = "utf-8",
filters = None
)
except Exception as e:
self.theParent.makeAlert(
["Failed to convert file using pypandoc + Pandoc.",
str(e)], nwAlert.ERROR
)
return False
return True
def _doClose(self):
logger.verbose("GuiExport close button clicked")
# General Settings
wNovel = self.tabMain.expNovel.isChecked()
wNotes = self.tabMain.expNotes.isChecked()
eFormat = self.tabMain.outputFormat.currentData()
fixWidth = self.tabMain.fixedWidth.value()
wComments = self.tabMain.expComments.isChecked()
wKeywords = self.tabMain.expKeywords.isChecked()
chFormat = self.tabMain.chapterFormat.text()
unFormat = self.tabMain.unnumFormat.text()
scFormat = self.tabMain.sceneFormat.text()
seFormat = self.tabMain.sectionFormat.text()
saveTo = self.tabMain.exportPath.text()
hScene = self.tabMain.hideScene.isChecked()
hSection = self.tabMain.hideSection.isChecked()
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
self.optState.setValue("GuiExport", "wNovel", wNovel)
self.optState.setValue("GuiExport", "wNotes", wNotes)
self.optState.setValue("GuiExport", "eFormat", eFormat)
self.optState.setValue("GuiExport", "fixWidth", fixWidth)
self.optState.setValue("GuiExport", "wComments", wComments)
self.optState.setValue("GuiExport", "wKeywords", wKeywords)
self.optState.setValue("GuiExport", "chFormat", chFormat)
self.optState.setValue("GuiExport", "unFormat", unFormat)
self.optState.setValue("GuiExport", "scFormat", scFormat)
self.optState.setValue("GuiExport", "seFormat", seFormat)
self.optState.setValue("GuiExport", "saveTo", saveTo)
self.optState.setValue("GuiExport", "hScene", hScene)
self.optState.setValue("GuiExport", "hSection", hSection)
# Pandoc Settings
pFormat = self.tabPandoc.outputFormat.currentData()
self.optState.setValue("GuiExport", "pFormat", pFormat)
self.optState.saveSettings()
self.close()
return
# END Class GuiExport
class GuiExportMain(QWidget):
FMT_NWD = 1 # novelWriter markdown
FMT_TXT = 2 # Plain text file
FMT_MD = 3 # Markdown file
FMT_HTML = 4 # HTML file
FMT_TEX = 5 # LaTeX file
FMT_PDOC = 6 # Pass to pandoc
FMT_EXT = {
FMT_NWD : ".nwd",
FMT_TXT : ".txt",
FMT_MD : ".md",
FMT_HTML : ".htm",
FMT_TEX : ".tex",
FMT_PDOC : ".tmp",
}
FMT_HELP = {
FMT_NWD : (
"Exports a document using the novelWriter markdown format. "
"The files selected by the filters are appended as-is, "
"including comments and other settings."
),
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."
),
FMT_HTML : (
"Exports a plain html5 file. Comments are wrapped in "
"blocks with a yellow background colour."
),
FMT_TEX : (
"Exports a LaTeX file that can be compiled to PDF using "
"for instance PDFLaTeX. Comments are exported as LaTeX "
"comments."
),
FMT_PDOC : (
"Exports first to markdown or html5. The file is then "
"passed on to Pandoc for a second stage. Use the Pandoc "
"tab for settings up the conversion."
),
}
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.outerBox = QGridLayout()
self.optState = self.theProject.optState
self.currFormat = self.FMT_TXT
# Select Files
self.guiFiles = QGroupBox("Selection", self)
self.guiFilesForm = QGridLayout(self)
self.guiFiles.setLayout(self.guiFilesForm)
self.expNovel = QCheckBox("Novel files",self)
self.expNovel.setChecked(
self.optState.getBool("GuiExport", "wNovel", True)
)
self.expNovel.setToolTip("Include all novel files in the exported document")
self.expNotes = QCheckBox("Note files",self)
self.expNotes.setChecked(
self.optState.getBool("GuiExport", "wNotes", False)
)
self.expNotes.setToolTip("Include all note files in the exported document")
self.expComments = QCheckBox("Comments",self)
self.expComments.setChecked(
self.optState.getBool("GuiExport", "wComments", False)
)
self.expComments.setToolTip("Export comments from all files")
self.expKeywords = QCheckBox("Keywords",self)
self.expKeywords.setChecked(
self.optState.getBool("GuiExport", "wKeywords", False)
)
self.expKeywords.setToolTip("Export @keywords from all files")
self.guiFilesForm.addWidget(self.expNovel, 0, 1)
self.guiFilesForm.addWidget(self.expComments, 0, 2)
self.guiFilesForm.addWidget(self.expNotes, 1, 1)
self.guiFilesForm.addWidget(self.expKeywords, 1, 2)
self.guiFilesForm.setRowStretch(2, 1)
# Chapter Settings
self.guiChapters = QGroupBox("Chapter Headings", self)
self.guiChaptersForm = QGridLayout(self)
self.guiChapters.setLayout(self.guiChaptersForm)
self.chapterFormat = QLineEdit()
self.chapterFormat.setMaxLength(200)
self.chapterFormat.setText(
self.optState.getString("GuiExport", "chFormat", "Chapter %numword%")
)
self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%")
self.chapterFormat.setMinimumWidth(250)
self.unnumFormat = QLineEdit()
self.unnumFormat.setMaxLength(200)
self.unnumFormat.setText(
self.optState.getString("GuiExport", "unFormat", "%title%")
)
self.unnumFormat.setToolTip("Available formats: %title%")
self.unnumFormat.setMinimumWidth(250)
self.guiChaptersForm.addWidget(QLabel("Numbered"), 0, 0)
self.guiChaptersForm.addWidget(self.chapterFormat, 0, 1)
self.guiChaptersForm.addWidget(QLabel("Unnumbered"), 1, 0)
self.guiChaptersForm.addWidget(self.unnumFormat, 1, 1)
# Scene and Section Settings
self.guiScenes = QGroupBox("Other Headings", self)
self.guiScenesForm = QGridLayout(self)
self.guiScenes.setLayout(self.guiScenesForm)
self.sceneFormat = QLineEdit()
self.sceneFormat.setMaxLength(200)
self.sceneFormat.setText(
self.optState.getString("GuiExport", "scFormat", "* * *")
)
self.sceneFormat.setToolTip("Available formats: %title%")
self.sceneFormat.setMinimumWidth(100)
self.sectionFormat = QLineEdit()
self.sectionFormat.setMaxLength(200)
self.sectionFormat.setText(
self.optState.getString("GuiExport", "seFormat", "")
)
self.sectionFormat.setToolTip("Available formats: %title%")
self.sectionFormat.setMinimumWidth(100)
self.hideScene = QCheckBox("Skip",self)
self.hideScene.setChecked(
self.optState.getBool("GuiExport", "hScene", False)
)
self.hideScene.setToolTip("Skip scene titles in export")
self.hideSection = QCheckBox("Skip",self)
self.hideSection.setChecked(
self.optState.getBool("GuiExport", "hSection", False)
)
self.hideSection.setToolTip("Skip section titles in export")
self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
self.guiScenesForm.addWidget(self.sceneFormat, 0, 1)
self.guiScenesForm.addWidget(self.hideScene, 0, 2)
self.guiScenesForm.addWidget(QLabel("Sections"), 1, 0)
self.guiScenesForm.addWidget(self.sectionFormat, 1, 1)
self.guiScenesForm.addWidget(self.hideSection, 1, 2)
# Output Path
self.exportTo = QGroupBox("Export Folder", self)
self.exportToForm = QGridLayout(self)
self.exportTo.setLayout(self.exportToForm)
self.exportPath = QLineEdit(
self.optState.getString("GuiExport", "saveTo", "")
)
self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"")
self.exportGetPath.clicked.connect(self._exportFolder)
self.exportToForm.addWidget(QLabel("Save to"), 0, 0)
self.exportToForm.addWidget(self.exportPath, 0, 1)
self.exportToForm.addWidget(self.exportGetPath, 0, 2)
# Output Format
self.guiOutput = QGroupBox("Export", self)
self.guiOutputForm = QGridLayout(self)
self.guiOutput.setLayout(self.guiOutputForm)
self.outputHelp = QLabel("")
self.outputHelp.setWordWrap(True)
self.outputHelp.setMinimumHeight(55)
self.outputHelp.setAlignment(Qt.AlignTop)
self.outputFormat = QComboBox(self)
self.outputFormat.addItem("novelWriter Markdown (.nwd)", self.FMT_NWD)
self.outputFormat.addItem("Plain Text (.txt)", self.FMT_TXT)
self.outputFormat.addItem("Markdown (.md)", self.FMT_MD)
self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML)
self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX)
self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC)
self.outputFormat.currentIndexChanged.connect(self._updateFormat)
optIdx = self.outputFormat.findData(
self.optState.getInt("GuiExport", "eFormat", 1)
)
if optIdx == -1:
self.outputFormat.setCurrentIndex(1)
self._updateFormat(1)
else:
self.outputFormat.setCurrentIndex(optIdx)
self._updateFormat(optIdx)
self.guiOutputForm.addWidget(QLabel("Format"), 0, 0)
self.guiOutputForm.addWidget(self.outputFormat, 0, 1)
self.guiOutputForm.addWidget(self.outputHelp, 1, 0, 1, 3)
self.guiOutputForm.setColumnStretch(2, 1)
# Additional Settings
self.addSettings = QGroupBox("Additional Settings (Format Dependent)", self)
self.addSettingsForm = QGridLayout(self)
self.addSettings.setLayout(self.addSettingsForm)
self.fixedWidth = QSpinBox(self)
self.fixedWidth.setMinimum(0)
self.fixedWidth.setMaximum(999)
self.fixedWidth.setSingleStep(1)
self.fixedWidth.setValue(
self.optState.getInt("GuiExport", "fixWidth", 80)
)
self.fixedWidth.setToolTip(
"Applies to .txt and .md files. A value of '0' disables the feature."
)
self.addSettingsForm.addWidget(QLabel("Fixed width"), 0, 0)
self.addSettingsForm.addWidget(self.fixedWidth, 0, 1)
self.addSettingsForm.setColumnStretch(2, 1)
# Assemble
self.outerBox.addWidget(self.guiOutput, 0, 0, 1, 2)
self.outerBox.addWidget(self.guiFiles, 0, 2)
self.outerBox.addWidget(self.guiChapters, 1, 0, 1, 2)
self.outerBox.addWidget(self.guiScenes, 1, 2)
self.outerBox.addWidget(self.addSettings, 2, 0, 1, 3)
self.outerBox.addWidget(self.exportTo, 3, 0, 1, 3)
self.outerBox.setColumnStretch(0, 1)
self.outerBox.setColumnStretch(1, 1)
self.outerBox.setColumnStretch(2, 1)
self.setLayout(self.outerBox)
return
##
# Internal Functions
##
def _updateFormat(self, currIdx):
"""Update help text under output format selection and file
extension in file box
"""
if currIdx == -1:
self.outputHelp.setText("")
else:
self.currFormat = self.outputFormat.itemData(currIdx)
self.outputHelp.setText("<i>%s</i>" % self.FMT_HELP[self.currFormat])
self._checkFileExtension()
return
def _exportFolder(self):
currDir = self.exportPath.text()
if not path.isdir(currDir):
currDir = ""
extFilter = [
"novelWriter document files (*.nwd)",
"Text files (*.txt)",
"Markdown files (*.md)",
"HTML files (*.htm *.html)",
"LaTeX files (*.tex)",
"All files (*.*)",
]
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Export File", self.exportPath.text(),
options=dlgOpt, filter=";;".join(extFilter)
)
if saveTo:
self.exportPath.setText(saveTo[0])
self._checkFileExtension()
return True
return False
def _checkFileExtension(self):
saveTo = self.exportPath.text()
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
fileBits = path.splitext(saveTo)
if self.currFormat > 0 and fileBits[0].strip() != "":
saveTo = fileBits[0]+self.FMT_EXT[self.currFormat]
self.exportPath.setText(saveTo)
return
# END Class GuiExportMain
class GuiExportPandoc(QWidget):
FMT_ODT = 1
FMT_DOCX = 2
FMT_EPUB2 = 4
FMT_EPUB3 = 5
FMT_ZIM = 6
FMT_EXT = {
FMT_ODT : ".odt",
FMT_DOCX : ".docx",
FMT_EPUB2 : ".epub",
FMT_EPUB3 : ".epub",
FMT_ZIM : ".txt",
}
FMT_VIA = {
FMT_ODT : "html",
FMT_DOCX : "html",
FMT_EPUB2 : "markdown",
FMT_EPUB3 : "markdown",
FMT_ZIM : "markdown",
}
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.outerBox = QGridLayout()
self.optState = self.theProject.optState
try:
import pypandoc
self.hasPyPan = True
except:
self.hasPyPan = False
# Information
self.guiInfo = QGroupBox("Information", self)
self.guiInfoBox = QVBoxLayout(self)
self.guiInfo.setLayout(self.guiInfoBox)
self.infoHelp = QLabel("")
self.infoHelp.setWordWrap(True)
self.infoHelp.setMinimumHeight(55)
self.infoHelp.setAlignment(Qt.AlignTop)
self.guiInfoBox.addWidget(self.infoHelp)
if self.hasPyPan:
self.infoHelp.setText((
"Additional export to other document formats than in the Settings tab is provided "
"by Pandoc. the project is first exported to Markdown or HTML, depending on final "
"format, and then processed by Pandoc into the desired format."
))
else:
self.infoHelp.setText((
"The Python package 'pypandoc' is not installed or isn't working. This package is "
"required for interfacing with Pandoc. Please install it before proceeding."
))
# Output Format
self.guiOutput = QGroupBox("Pandoc Format", self)
self.guiOutputForm = QGridLayout(self)
self.guiOutput.setLayout(self.guiOutputForm)
self.outputFormat = QComboBox(self)
self.outputFormat.addItem("Open Office Document (.odt)", self.FMT_ODT)
self.outputFormat.addItem("Word Document (.docx)", self.FMT_DOCX)
self.outputFormat.addItem("ePUB eBook v2 (.epub2)", self.FMT_EPUB2)
self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3)
self.outputFormat.addItem("Zim Wiki (.txt)", self.FMT_ZIM)
optIdx = self.outputFormat.findData(
self.optState.getInt("GuiExport", "pFormat", 1)
)
if optIdx == -1:
self.outputFormat.setCurrentIndex(1)
else:
self.outputFormat.setCurrentIndex(optIdx)
self.guiOutputForm.addWidget(QLabel("Format"), 0, 0)
self.guiOutputForm.addWidget(self.outputFormat, 0, 1)
self.guiOutputForm.setColumnStretch(2, 1)
# Assemble
self.outerBox.addWidget(self.guiInfo, 0, 0)
self.outerBox.addWidget(self.guiOutput, 1, 0)
self.outerBox.setRowStretch(2, 1)
self.setLayout(self.outerBox)
return
# END Class GuiExportPandoc
+45 -21
View File
@@ -30,10 +30,11 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit,
QPushButton, QComboBox
QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QGridLayout, QLineEdit,
QComboBox, QLabel, QSpacerItem, QSizePolicy, QDialogButtonBox
)
from nw.gui.additions import QSwitch
from nw.constants import nwLabels, nwItemLayout, nwItemClass, nwItemType
logger = logging.getLogger(__name__)
@@ -43,27 +44,31 @@ class GuiItemEditor(QDialog):
def __init__(self, theParent, theProject, tHandle):
QDialog.__init__(self, theParent)
logger.debug("Initialising ItemEditor ...")
logger.debug("Initialising GuiItemEditor ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theItem = self.theProject.projTree[tHandle]
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.theItem = self.theProject.projTree[tHandle]
if self.theItem is None:
self._doClose()
self.setWindowTitle("Item Settings")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64))
self.outerBox.setSpacing(16)
self.setLayout(self.outerBox)
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.mainGroup = QGroupBox("Item Settings")
self.mainForm = QFormLayout()
self.mainForm = QGridLayout()
self.editName = QLineEdit()
self.editName.setMinimumWidth(220)
self.editName.setMaxLength(200)
self.editStatus = QComboBox()
@@ -100,11 +105,26 @@ class GuiItemEditor(QDialog):
if itemLayout in self.validLayouts:
self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout],itemLayout)
self.mainForm.addRow("Label", self.editName)
self.mainForm.addRow("Status", self.editStatus)
self.mainForm.addRow("Layout", self.editLayout)
self.textExport = QLabel("Include when building project")
self.editExport = QSwitch()
if self.theItem.itemType == nwItemType.FILE:
self.editExport.setEnabled(True)
self.editExport.setChecked(self.theItem.isExported)
else:
self.editExport.setEnabled(False)
self.editExport.setChecked(False)
self.editName.setMinimumWidth(200)
self.mainForm.addWidget(QLabel("Label"), 0, 0)
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
self.mainForm.addWidget(QLabel("Status"), 1, 0)
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
self.mainForm.addWidget(QLabel("Layout"), 2, 0)
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
self.mainForm.addWidget(self.textExport, 4, 0, 1, 2)
self.mainForm.addWidget(self.editExport, 4, 2)
self.spacerItem = QSpacerItem(12, 12, QSizePolicy.Fixed, QSizePolicy.Fixed)
self.mainForm.addItem(self.spacerItem, 3, 0)
self.editName.setText(self.theItem.itemName)
statusIdx = self.editStatus.findData(self.theItem.itemStatus)
@@ -114,39 +134,43 @@ class GuiItemEditor(QDialog):
if layoutIdx != -1:
self.editLayout.setCurrentIndex(layoutIdx)
self.buttonBox = QHBoxLayout()
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.saveButton = QPushButton("Save")
self.saveButton.setDefault(True)
self.saveButton.clicked.connect(self._doSave)
self.buttonBox.addStretch(1)
self.buttonBox.addWidget(self.closeButton)
self.buttonBox.addWidget(self.saveButton)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
self.mainGroup.setLayout(self.mainForm)
self.innerBox.addWidget(self.mainGroup)
self.innerBox.addLayout(self.buttonBox)
self.innerBox.addWidget(self.buttonBox)
self.show()
self.editName.selectAll()
logger.debug("ItemEditor initialisation complete")
logger.debug("GuiItemEditor initialisation complete")
return
def _doSave(self):
"""Save the setting to the item.
"""
logger.verbose("ItemEditor save button clicked")
itemName = self.editName.text()
itemStatus = self.editStatus.currentData()
itemLayout = self.editLayout.currentData()
isExported = self.editExport.isChecked()
self.theItem.setName(itemName)
self.theItem.setStatus(itemStatus)
self.theItem.setLayout(itemLayout)
self.theItem.setExported(isExported)
self.theProject.setProjectChanged(True)
self.accept()
self.close()
return
def _doClose(self):
+3 -2
View File
@@ -54,10 +54,10 @@ class GuiProjectEditor(QDialog):
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Project Settings")
self.setLayout(self.outerBox)
self.setWindowTitle("Project Settings")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64))
self.outerBox.setSpacing(16)
self.theProject.countStatus()
self.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
@@ -78,6 +78,7 @@ class GuiProjectEditor(QDialog):
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
self.setLayout(self.outerBox)
self.innerBox.addWidget(self.tabWidget)
self.innerBox.addWidget(self.buttonBox)
+115 -28
View File
@@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel
from nw.constants import nwLabels
from nw.constants import nwLabels, nwItemClass, nwItemType, nwUnicode
logger = logging.getLogger(__name__)
@@ -48,7 +48,7 @@ class GuiDocDetails(QFrame):
self.mainBox = QGridLayout(self)
self.mainBox.setVerticalSpacing(1)
self.mainBox.setHorizontalSpacing(15)
self.mainBox.setHorizontalSpacing(6)
self.setLayout(self.mainBox)
self.fntOne = QFont()
@@ -56,50 +56,137 @@ class GuiDocDetails(QFrame):
self.fntOne.setBold(True)
self.fntTwo = QFont()
self.fntTwo.setFamily("Monospace")
self.fntTwo.setPointSize(10)
self.colTwo = [
QLabel(""),
QLabel(""),
QLabel(""),
QLabel("")
]
colOne = ["Label","Status","Class","Layout"]
for nRow in range(4):
lblOne = QLabel(colOne[nRow])
lblOne.setFont(self.fntOne)
lblOne.setAlignment(Qt.AlignTop)
self.mainBox.addWidget(lblOne,nRow,0)
self.mainBox.addWidget(self.colTwo[nRow],nRow,1)
self.colTwo[nRow].setWordWrap(True)
self.colTwo[nRow].setAlignment(Qt.AlignTop)
self.fntThree = QFont()
self.fntThree.setPointSize(10)
# Label
self.labelName = QLabel("Label ")
self.labelName.setFont(self.fntOne)
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelFlag = QLabel("")
self.labelFlag.setFont(self.fntTwo)
self.labelFlag.setAlignment(Qt.AlignRight | Qt.AlignBaseline)
self.labelData = QLabel("")
self.labelData.setFont(self.fntThree)
self.labelData.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelData.setWordWrap(True)
# Status
self.statusName = QLabel("Status ")
self.statusName.setFont(self.fntOne)
self.statusName.setAlignment(Qt.AlignLeft)
self.statusFlag = QLabel("")
self.statusFlag.setFont(self.fntTwo)
self.statusFlag.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.statusData = QLabel("")
self.statusData.setFont(self.fntThree)
self.statusData.setAlignment(Qt.AlignLeft)
# Class
self.className = QLabel("Class ")
self.className.setFont(self.fntOne)
self.className.setAlignment(Qt.AlignLeft)
self.classFlag = QLabel("")
self.classFlag.setFont(self.fntTwo)
self.classFlag.setAlignment(Qt.AlignRight)
self.classData = QLabel("")
self.classData.setFont(self.fntThree)
self.classData.setAlignment(Qt.AlignLeft)
# Layout
self.layoutName = QLabel("Layout ")
self.layoutName.setFont(self.fntOne)
self.layoutName.setAlignment(Qt.AlignLeft)
self.layoutFlag = QLabel("")
self.layoutFlag.setFont(self.fntTwo)
self.layoutFlag.setAlignment(Qt.AlignRight)
self.layoutData = QLabel("")
self.layoutData.setFont(self.fntThree)
self.layoutData.setAlignment(Qt.AlignLeft)
# Assemble
self.mainBox.addWidget(self.labelName, 0, 0)
self.mainBox.addWidget(self.statusName, 1, 0)
self.mainBox.addWidget(self.className, 2, 0)
self.mainBox.addWidget(self.layoutName, 3, 0)
self.mainBox.addWidget(self.labelFlag, 0, 1)
self.mainBox.addWidget(self.statusFlag, 1, 1)
self.mainBox.addWidget(self.classFlag, 2, 1)
self.mainBox.addWidget(self.layoutFlag, 3, 1)
self.mainBox.addWidget(self.labelData, 0, 2)
self.mainBox.addWidget(self.statusData, 1, 2)
self.mainBox.addWidget(self.classData, 2, 2)
self.mainBox.addWidget(self.layoutData, 3, 2)
self.mainBox.setColumnStretch(0,0)
self.mainBox.setColumnStretch(1,1)
self.mainBox.setColumnStretch(1,0)
self.mainBox.setColumnStretch(2,1)
logger.debug("DocDetails initialisation complete")
return
def buildViewBox(self, tHandle):
###
# Class Methods
##
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
colTwo = [""]*4
self.labelFlag.setText("")
self.statusFlag.setText("")
self.classFlag.setText("")
self.layoutFlag.setText("")
self.labelData.setText("")
self.statusData.setText("")
self.classData.setText("")
self.layoutData.setText("")
else:
theLabel = nwItem.itemName
if len(theLabel) > 100:
theLabel = theLabel[:96].rstrip()+" ..."
colTwo = [
theLabel,
nwItem.itemStatus,
nwLabels.CLASS_NAME[nwItem.itemClass],
nwLabels.LAYOUT_NAME[nwItem.itemLayout],
]
for nRow in range(4):
self.colTwo[nRow].setText(colTwo[nRow])
iStatus = nwItem.itemStatus
if nwItem.itemClass == nwItemClass.NOVEL:
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.statusIcons[iStatus]
else:
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.importIcons[iStatus]
if nwItem.itemType == nwItemType.FILE:
if nwItem.isExported:
exportFlag = nwUnicode.U_CHECK
else:
exportFlag = " "
else:
exportFlag = "+"
self.labelFlag.setText(exportFlag)
self.statusFlag.setPixmap(flagIcon.pixmap(10, 10))
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout])
self.labelData.setText(theLabel)
self.statusData.setText(nwItem.itemStatus)
self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass])
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
return
+9 -1
View File
@@ -32,7 +32,7 @@ from time import time
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtWidgets import (
qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QLabel
qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox
)
from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
@@ -352,6 +352,14 @@ class GuiDocEditor(QTextEdit):
return
def updateDocTitle(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self.theHandle:
self.docTitle.setTitleFromHandle(self.theHandle)
return
##
# Setters and Getters
##
+19 -6
View File
@@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
from nw.core import NWDoc
from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwUnicode
)
logger = logging.getLogger(__name__)
@@ -254,6 +254,8 @@ class GuiDocTree(QTreeWidget):
return theList
def getColumnSizes(self):
"""Return the column widths for the tree columns.
"""
retVals = [
self.columnWidth(0),
self.columnWidth(1),
@@ -401,7 +403,8 @@ class GuiDocTree(QTreeWidget):
return True
def setTreeItemValues(self, tHandle):
"""Set the name and flag values for a tree item.
"""
trItem = self._getTreeItem(tHandle)
nwItem = self.theProject.projTree[tHandle]
tName = nwItem.itemName
@@ -409,9 +412,19 @@ class GuiDocTree(QTreeWidget):
tHandle = nwItem.itemHandle
pHandle = nwItem.parHandle
tStatus = nwLabels.CLASS_FLAG[nwItem.itemClass]
stExport = " "
stClass = nwLabels.CLASS_FLAG[nwItem.itemClass]
stLayout = ""
if nwItem.itemType == nwItemType.FILE:
tStatus += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
stLayout = "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
if nwItem.isExported:
stExport = nwUnicode.U_CHECK
else:
stExport = "+"
tStatus = stExport+" "+stClass+stLayout
iStatus = nwItem.itemStatus
if tClass == nwItemClass.NOVEL:
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
@@ -519,8 +532,8 @@ class GuiDocTree(QTreeWidget):
newItem.setText(self.C_HANDLE, tHandle)
# newItem.setForeground(self.C_COUNT,QColor(*self.theParent.theTheme.treeWCount))
newItem.setTextAlignment(self.C_COUNT,Qt.AlignRight)
newItem.setFont(self.C_FLAGS,self.fontFlags)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setFont(self.C_FLAGS, self.fontFlags)
self.theMap[tHandle] = newItem
if pHandle is None:
+9 -1
View File
@@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTextBrowser
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
from nw.convert import ToHtml
from nw.core import ToHtml
from nw.constants import nwAlert, nwItemType, nwDocAction
from nw.gui.elements.doctitlebar import GuiDocTitleBar
@@ -192,6 +192,14 @@ class GuiDocViewer(QTextBrowser):
return False
return True
def updateDocTitle(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self.theHandle:
self.docTitle.setTitleFromHandle(self.theHandle)
return
##
# Events
##
+6 -6
View File
@@ -225,11 +225,11 @@ class GuiMainMenu(QMenuBar):
self.projMenu.addAction(self.aProjectSettings)
# Project > Export Project
self.aExportProject = QAction("Export Project", self)
self.aExportProject.setStatusTip("Export project")
self.aExportProject.setShortcut("F5")
self.aExportProject.triggered.connect(self.theParent.exportProjectDialog)
self.projMenu.addAction(self.aExportProject)
self.aBuildProject = QAction("Build Project", self)
self.aBuildProject.setStatusTip("Build project")
self.aBuildProject.setShortcut("F5")
self.aBuildProject.triggered.connect(self.theParent.buildProjectDialog)
self.projMenu.addAction(self.aBuildProject)
# Project > Session Log
self.aSessionLog = QAction("Session Log", self)
@@ -352,7 +352,7 @@ class GuiMainMenu(QMenuBar):
self.docuMenu.addAction(self.aCloseView)
# Document > Toggle View Comments
self.aViewDocComments = QAction("View Comments", self)
self.aViewDocComments = QAction("Show Comments", self)
self.aViewDocComments.setStatusTip("Show comments in view panel")
self.aViewDocComments.setCheckable(True)
self.aViewDocComments.setChecked(self.mainConf.viewComments)
-1
View File
@@ -32,7 +32,6 @@ import nw
from os import path
from nw.common import checkString, checkBool, checkInt
from nw.constants import nwFiles
logger = logging.getLogger(__name__)
+9 -6
View File
@@ -40,12 +40,12 @@ from PyQt5.QtWidgets import (
)
from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline,
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel
)
from nw.core import NWProject, NWDoc, NWIndex, countWords
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwFiles, nwItemType, nwAlert
logger = logging.getLogger(__name__)
@@ -629,6 +629,9 @@ class GuiMain(QMainWindow):
dlgProj = GuiItemEditor(self, self.theProject, tHandle)
if dlgProj.exec_():
self.treeView.setTreeItemValues(tHandle)
self.treeMeta.updateViewBox(tHandle)
self.docEditor.updateDocTitle(tHandle)
self.docViewer.updateDocTitle(tHandle)
return
@@ -755,9 +758,9 @@ class GuiMain(QMainWindow):
self._setWindowTitle(self.theProject.projName)
return True
def exportProjectDialog(self):
def buildProjectDialog(self):
if self.hasProject:
dlgExport = GuiExport(self, self.theProject)
dlgExport = GuiBuildNovel(self, self.theProject)
dlgExport.exec_()
return True
@@ -1006,7 +1009,7 @@ class GuiMain(QMainWindow):
def _treeSingleClick(self):
sHandle = self.treeView.getSelectedHandle()
if sHandle is not None:
self.treeMeta.buildViewBox(sHandle)
self.treeMeta.updateViewBox(sHandle)
return
def _treeDoubleClick(self, tItem, colNo):
-2
View File
@@ -1,5 +1,3 @@
pyqt5
lxml
pyenchant
latexcodec
pypandoc
+25 -1
View File
@@ -11,13 +11,23 @@
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>96b68994dfa3d</lastEdited>
<lastViewed>b3e74dbc1f584</lastViewed>
<lastViewed>6a2d6d5f4f401</lastViewed>
<lastWordCount>875</lastWordCount>
<autoReplace>
<A>B</A>
<B>E</B>
<C>D</C>
</autoReplace>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %chnum%.\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>Scene %chnum%.%scnum%: %title%</scene>
<section></section>
<withSynopsis>True</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Notes</entry>
@@ -48,6 +58,7 @@
<class>NOVEL</class>
<status>Started</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>TITLE</layout>
<charCount>72</charCount>
<wordCount>15</wordCount>
@@ -67,6 +78,7 @@
<class>NOVEL</class>
<status>Notes</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>CHAPTER</layout>
<charCount>12</charCount>
<wordCount>3</wordCount>
@@ -79,6 +91,7 @@
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>1199</charCount>
<wordCount>216</wordCount>
@@ -91,6 +104,7 @@
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>476</charCount>
<wordCount>93</wordCount>
@@ -103,6 +117,7 @@
<class>NOVEL</class>
<status>Finished</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>UNNUMBERED</layout>
<charCount>633</charCount>
<wordCount>101</wordCount>
@@ -115,6 +130,7 @@
<class>NOVEL</class>
<status>2nd Draft</status>
<expanded>False</expanded>
<exported>False</exported>
<layout>NOTE</layout>
<charCount>1692</charCount>
<wordCount>313</wordCount>
@@ -127,6 +143,7 @@
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>CHAPTER</layout>
<charCount>139</charCount>
<wordCount>28</wordCount>
@@ -139,6 +156,7 @@
<class>NOVEL</class>
<status>1st Draft</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>189</charCount>
<wordCount>37</wordCount>
@@ -165,6 +183,7 @@
<class>CHARACTER</class>
<status>Minor</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>49</charCount>
<wordCount>9</wordCount>
@@ -177,6 +196,7 @@
<class>CHARACTER</class>
<status>Major</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>55</charCount>
<wordCount>9</wordCount>
@@ -196,6 +216,7 @@
<class>WORLD</class>
<status>Main</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>76</charCount>
<wordCount>15</wordCount>
@@ -208,6 +229,7 @@
<class>WORLD</class>
<status>Minor</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>115</charCount>
<wordCount>24</wordCount>
@@ -220,6 +242,7 @@
<class>WORLD</class>
<status>Major</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>28</charCount>
<wordCount>6</wordCount>
@@ -239,6 +262,7 @@
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>30</charCount>
<wordCount>6</wordCount>
+12 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.5" fileVersion="1.0" timeStamp="2019-06-27 20:30:38">
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-10 23:17:36">
<project>
<name></name>
<title></title>
@@ -12,6 +12,16 @@
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
@@ -46,6 +56,7 @@
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
+15 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.4.5" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-07 22:03:17">
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-10 23:18:06">
<project>
<name></name>
<title></title>
@@ -12,6 +12,16 @@
<lastViewed>31489056e0916</lastViewed>
<lastWordCount>86</lastWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
@@ -46,6 +56,7 @@
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>331</charCount>
<wordCount>59</wordCount>
@@ -65,6 +76,7 @@
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>34</charCount>
<wordCount>8</wordCount>
@@ -84,6 +96,7 @@
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>48</charCount>
<wordCount>10</wordCount>
@@ -103,6 +116,7 @@
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>51</charCount>
<wordCount>9</wordCount>
+12 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 21:05:51">
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-10 23:19:00">
<project>
<name>Project Name</name>
<title>Project Title</title>
@@ -16,6 +16,16 @@
<autoReplace>
<This>With This Stuff </This>
</autoReplace>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
@@ -50,6 +60,7 @@
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
+12 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:49:50">
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="2" autoCount="0" timeStamp="2020-05-10 23:19:44">
<project>
<name></name>
<title></title>
@@ -12,6 +12,16 @@
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
@@ -46,6 +56,7 @@
<class>NOVEL</class>
<status>Note</status>
<expanded>False</expanded>
<exported>False</exported>
<layout>PAGE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
+12 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:52:21">
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="1" autoCount="0" timeStamp="2020-05-10 23:16:14">
<project>
<name></name>
<title></title>
@@ -12,6 +12,16 @@
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
@@ -67,6 +77,7 @@
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
+12 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.4.5" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-07 22:52:18">
<novelWriterXML appVersion="0.5" fileVersion="1.0" saveCount="4" autoCount="0" timeStamp="2020-05-10 23:16:52">
<project>
<name></name>
<title></title>
@@ -12,6 +12,16 @@
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %num%\\%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
<withSynopsis>False</withSynopsis>
<withComments>False</withComments>
<withKeywords>False</withKeywords>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
@@ -67,6 +77,7 @@
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
+6 -3
View File
@@ -13,7 +13,7 @@ from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.constants import *
keyDelay = 10
keyDelay = 5
stepDelay = 50
@pytest.mark.gui
@@ -361,7 +361,10 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.PAGE)
itemEdit.editLayout.setCurrentIndex(layoutIdx)
qtbot.mouseClick(itemEdit.saveButton, Qt.LeftButton)
itemEdit.editExport.setChecked(False)
assert not itemEdit.editExport.isChecked()
itemEdit._doSave()
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916")
qtbot.addWidget(itemEdit)
@@ -369,7 +372,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
assert itemEdit.editStatus.currentData() == "Note"
assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE
qtbot.mouseClick(itemEdit.closeButton, Qt.LeftButton)
itemEdit._doClose()
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
+2 -1
View File
@@ -197,7 +197,8 @@ def testItemXMLPackUnpack():
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b"<content>"
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
b"<name>A Name</name><type>TRASH</type><class>TRASH</class><status>Main</status><expanded>True</expanded>"
b"<name>A Name</name><type>TRASH</type><class>TRASH</class>"
b"<status>Main</status><expanded>True</expanded>"
b"</item>"
b"</content>"
)