From b9608dcdbdb154d0a4ef2a533c161200855d9b50 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 25 Oct 2019 00:07:54 +0200 Subject: [PATCH] Add support for LaTeX export --- nw/convert/latexfile.py | 66 +++++++++++++++++ nw/convert/tolatex.py | 155 ++++++++++++++++++++++++++++++++++++++++ nw/gui/export.py | 7 +- 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 nw/convert/latexfile.py create mode 100644 nw/convert/tolatex.py diff --git a/nw/convert/latexfile.py b/nw/convert/latexfile.py new file mode 100644 index 00000000..448236ff --- /dev/null +++ b/nw/convert/latexfile.py @@ -0,0 +1,66 @@ +# -*- 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] + +""" + +import logging +import nw + +from nw.convert.textfile import TextFile +from nw.convert.tolatex import ToLaTeX +from nw.enum 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) + + return + + ## + # Internal Functions + ## + + def _doOpenFile(self, filePath): + + if self.winEnding: + tN = "\r\n" + else: + tN = "\n" + + try: + self.outFile = open(filePath,mode="w+") + self.outFile.write(r"\documentclass[12pt]{report}"+tN+tN) + self.outFile.write(r"\usepackage[utf8]{inputenc}"+tN+tN) + self.outFile.write(r"\begin{document}"+tN+tN) + except Exception as e: + self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR) + return False + + return True + + def _doCloseFile(self): + + if self.winEnding: + tN = "\r\n" + else: + tN = "\n" + + if self.outFile is not None: + self.outFile.write(r"\end{document}"+tN) + self.outFile.close() + + return True + +# END Class LaTeXFile diff --git a/nw/convert/tolatex.py b/nw/convert/tolatex.py new file mode 100644 index 00000000..7b8a6f51 --- /dev/null +++ b/nw/convert/tolatex.py @@ -0,0 +1,155 @@ +# -*- 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] + +""" + +import textwrap +import logging +import re +import nw + +from nw.convert.tokenizer import Tokenizer + +logger = logging.getLogger(__name__) + +class ToLaTeX(Tokenizer): + + def __init__(self, theProject, theParent): + Tokenizer.__init__(self, theProject, theParent) + return + + def doAutoReplace(self): + Tokenizer.doAutoReplace(self) + + repDict = { + "\u2013" : "--", + "\u2014" : "---", + "\u2500" : "---", + "\u2026" : "...", + } + 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): + + 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"}", + } + + 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 + ) + tComm = textwrap.TextWrapper( + width = self.wordWrap-2, + 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: + + 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) + + # 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: + aText = tComm.wrap(tText) + tText = "\n% ".join(aText) + 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: + self.theResult += begText + self.theResult += "%s\n" % tWrap.fill(" ".join(thisPar)) + self.theResult += endText + thisPar = [] + + elif tType == self.T_HEAD1: + self.theResult += begText + self.theResult += "{\\Huge %s}\n" % tText + self.theResult += endText + + elif tType == self.T_HEAD2: + self.theResult += "\\chapter*{%s}\n\n" % tText + + elif tType == self.T_HEAD3: + self.theResult += "\\section*{%s}\n\n" % tText + + elif tType == self.T_HEAD4: + self.theResult += "\\subsection*{%s}\n\n" % tText + + elif tType == self.T_SEP: + self.theResult += begText + self.theResult += "%s\n" % tText + self.theResult += endText + + elif tType == self.T_TEXT: + thisPar.append(tText) + + 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_COMMAND and self.doCommands: + self.theResult += "%% @%s\n\n" % tText + + return + +# END Class ToLaTeX diff --git a/nw/gui/export.py b/nw/gui/export.py index 5cd16d3f..0fc82e6b 100644 --- a/nw/gui/export.py +++ b/nw/gui/export.py @@ -29,6 +29,7 @@ from nw.tools.optlaststate import OptLastState from nw.convert.textfile import TextFile from nw.convert.htmlfile import HtmlFile from nw.convert.markdownfile import MarkdownFile +from nw.convert.latexfile import LaTeXFile from nw.constants import nwFiles from nw.enum import nwItemType @@ -127,6 +128,8 @@ class GuiExport(QDialog): 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) if outFile is None: return False @@ -348,7 +351,7 @@ class GuiExportMain(QWidget): self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML) # self.outputFormat.addItem("HTML5 for eBook (.htm)", self.FMT_EBOOK) # self.outputFormat.addItem("Open Document (.odt)", self.FMT_ODT) - # self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX) + self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX) self.outputFormat.currentIndexChanged.connect(self._updateFormat) optIdx = self.outputFormat.findData(self.optState.getSetting("eFormat")) @@ -417,7 +420,7 @@ class GuiExportMain(QWidget): "Markdown files (*.md)", "HTML files (*.htm *.html)", # "Open document files (*.odt)", - # "LaTeX files (*.tex)", + "LaTeX files (*.tex)", "All files (*.*)", ]