diff --git a/nw/config.py b/nw/config.py index 695b31e0..e07852c6 100644 --- a/nw/config.py +++ b/nw/config.py @@ -19,7 +19,7 @@ from os import path, mkdir, makedirs, getcwd from appdirs import user_config_dir from datetime import datetime -from nw.constants import nwFiles +from nw.constants import nwFiles, nwUnicode from nw.common import splitVersionNumber from PyQt5.Qt import PYQT_VERSION_STR @@ -87,8 +87,8 @@ class Config: self.doReplaceDots = True self.wordCountTimer = 5.0 - self.fmtSingleQuotes = ["\u2018","\u2019"] - self.fmtDoubleQuotes = ["\u201c","\u201d"] + self.fmtSingleQuotes = [nwUnicode.U_LSQUO,nwUnicode.U_RSQUO] + self.fmtDoubleQuotes = [nwUnicode.U_LDQUO,nwUnicode.U_RDQUO] self.spellLanguage = "en_GB" diff --git a/nw/constants.py b/nw/constants.py index 4fa6ab02..92c693dc 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -142,26 +142,74 @@ class nwQuotes(): "\u300e", # Left white corner bracket "\u300f", # Right white corner bracket ] - HTML = { - "\u0022" : """, - "\u0027" : "'", - "\u00ab" : "«", - "\u00bb" : "»", - "\u2018" : "‘", - "\u2019" : "’", - "\u201a" : "‚", - "\u201b" : "‛", - "\u201c" : "“", - "\u201d" : "”", - "\u201e" : "„", - "\u201f" : "‟", - "\u2039" : "‹", - "\u203a" : "›", - "\u2e42" : "⹂", - "\u300c" : "「", - "\u300d" : "」", - "\u300e" : "『", - "\u300f" : "』", - } -# END Class nwQuotes \ No newline at end of file +# END Class nwQuotes + +class nwUnicode: + """Suppoted unicode character constants and translation maps. + """ + + # Unicode Constants + + ## Quotation Marks + U_QUOT = "\u0022" # Quotation mark + U_APOS = "\u0027" # Apostrophe + U_LAQUO = "\u00ab" # Left-pointing double angle quotation mark + U_RAQUO = "\u00bb" # Right-pointing double angle quotation mark + U_LSQUO = "\u2018" # Left single quotation mark + U_RSQUO = "\u2019" # Right single quotation mark + U_SBQUO = "\u201a" # Single low-9 quotation mark + U_SUQUO = "\u201b" # Single high-reversed-9 quotation mark + U_LDQUO = "\u201c" # Left double quotation mark + U_RDQUO = "\u201d" # Right double quotation mark + U_BDQUO = "\u201e" # Double low-9 quotation mark + U_UDQUO = "\u201f" # Double high-reversed-9 quotation mark + U_LSAQUO = "\u2039" # Single left-pointing angle quotation mark + U_RSAQUO = "\u203a" # Single right-pointing angle quotation mark + U_BDRQUO = "\u2e42" # Double low-reversed-9 quotation mark + U_LCQUO = "\u300c" # Left corner bracket + U_RCQUO = "\u300d" # Right corner bracket + U_LWCQUO = "\u300e" # Left white corner bracket + U_RECQUO = "\u300f" # Right white corner bracket + + ## Punctuation + U_ENDASH = "\u2013" # Short dash + U_EMDASH = "\u2014" # Long dash + U_HELLIP = "\u2026" # Ellipsis + + ## Other + U_NBSP = "\u00a0" # Non-breaking space + U_PARA = "\u2029" # Paragraph separator + + # HTML Equivalents + + ## Quotes + H_QUOT = """ + H_APOS = "'" + H_LAQUO = "«" + H_RAQUO = "»" + H_LSQUO = "‘" + H_RSQUO = "’" + H_SBQUO = "‚" + H_SUQUO = "‛" + H_LDQUO = "“" + H_RDQUO = "”" + H_BDQUO = "„" + H_UDQUO = "‟" + H_LSAQUO = "‹" + H_RSAQUO = "›" + H_BDRQUO = "⹂" + H_LCQUO = "「" + H_RCQUO = "」" + H_LWCQUO = "『" + H_LWCQUO = "『" + + ## Punctuation + H_ENDASH = "–" + H_EMDASH = "—" + H_HELLIP = "…" + + ## Other + H_NBSP = " " + +# END Class nwUnicode diff --git a/nw/convert/file/html.py b/nw/convert/file/html.py index 278acab7..1dc89b39 100644 --- a/nw/convert/file/html.py +++ b/nw/convert/file/html.py @@ -38,18 +38,19 @@ class HtmlFile(TextFile): self.outFile.write("\n") self.outFile.write("\n") self.outFile.write("\n") - self.outFile.write("\n") + self.outFile.write(" \n") + self.outFile.write(" \n") self.outFile.write("\n") self.outFile.write("\n") self.outFile.write("
\n") diff --git a/nw/convert/file/text.py b/nw/convert/file/text.py index 9c490b3d..9301a823 100644 --- a/nw/convert/file/text.py +++ b/nw/convert/file/text.py @@ -138,6 +138,7 @@ class TextFile(): 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) diff --git a/nw/convert/text/tohtml.py b/nw/convert/text/tohtml.py index 0c086a68..64ddbbb7 100644 --- a/nw/convert/text/tohtml.py +++ b/nw/convert/text/tohtml.py @@ -15,6 +15,7 @@ import re import nw from nw.convert.tokenizer import Tokenizer +from nw.constants import nwUnicode logger = logging.getLogger(__name__) @@ -28,13 +29,13 @@ class ToHtml(Tokenizer): Tokenizer.doAutoReplace(self) repDict = { - "<" : "<", - ">" : ">", - "&" : "&", - "\u2013" : "&endash;", - "\u2014" : "$emdash;", - "\u2500" : "$emdash;", - "\u2026" : "…", + "<" : "<", + ">" : ">", + "&" : "&", + 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) diff --git a/nw/convert/text/tolatex.py b/nw/convert/text/tolatex.py index cfcec012..435fee84 100644 --- a/nw/convert/text/tolatex.py +++ b/nw/convert/text/tolatex.py @@ -16,6 +16,7 @@ import re import nw from nw.convert.tokenizer import Tokenizer +from nw.constants import nwUnicode logger = logging.getLogger(__name__) @@ -26,6 +27,20 @@ class ToLaTeX(Tokenizer): 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 = { diff --git a/nw/convert/text/totext.py b/nw/convert/text/totext.py index fc21163e..5b226999 100644 --- a/nw/convert/text/totext.py +++ b/nw/convert/text/totext.py @@ -16,6 +16,7 @@ import re import nw from nw.convert.tokenizer import Tokenizer +from nw.constants import nwUnicode logger = logging.getLogger(__name__) @@ -25,6 +26,17 @@ class ToText(Tokenizer): Tokenizer.__init__(self, theProject, theParent) return + def doAutoReplace(self): + Tokenizer.doAutoReplace(self) + + repDict = { + 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. """ diff --git a/nw/convert/tokenizer.py b/nw/convert/tokenizer.py index 63abc1ae..7be25952 100644 --- a/nw/convert/tokenizer.py +++ b/nw/convert/tokenizer.py @@ -149,6 +149,9 @@ class Tokenizer(): 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 diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 03d3313d..1d0c35ac 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -23,7 +23,7 @@ from nw.project.document import NWDoc from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.wordcounter import WordCounter from nw.tools.spellcheck import NWSpellCheck -from nw.constants import nwFiles +from nw.constants import nwFiles, nwUnicode from nw.enum import nwDocAction, nwAlert logger = logging.getLogger(__name__) @@ -218,7 +218,14 @@ class GuiDocEditor(QTextEdit): return self.docChanged def getText(self): - theText = self.toPlainText() + """Get the text content of the current document. This method uses QTextEdit->toPlainText for + Qt versions lower than 5.9, and the QDocument->toRawText for higher version. The latter + preserves non-breaking spaces, which the former does not. + """ + if self.mainConf.verQtValue >= 50900: + theText = self.qDocument.toRawText().replace(nwUnicode.U_PARA,"\n") + else: + theText = self.toPlainText() return theText def setCursorPosition(self, thePosition): @@ -316,9 +323,26 @@ class GuiDocEditor(QTextEdit): to know whether we had a selection prior to triggering the _docChange slot, as we do not want to trigger autoreplace on selections. Autoreplace on selections messes with undo/redo history. + We also need to intercept the Shift key modifier for certain key combinations that modifies + standard keys like enter and space. However, we don't want to spend a lot of time in this + function as it is triggered on every keypress when typing. """ + self.hasSelection = self.textCursor().hasSelection() - QTextEdit.keyPressEvent(self, keyEvent) + + if keyEvent.modifiers() == Qt.ShiftModifier: + theKey = keyEvent.key() + if theKey == Qt.Key_Return: + self._insertHardBreak() + elif theKey == Qt.Key_Enter: + self._insertHardBreak() + elif theKey == Qt.Key_Space: + self._insertNonBreakingSpace() + else: + QTextEdit.keyPressEvent(self, keyEvent) + else: + QTextEdit.keyPressEvent(self, keyEvent) + return ## @@ -332,6 +356,13 @@ class GuiDocEditor(QTextEdit): theCursor.endEditBlock() return + def _insertNonBreakingSpace(self): + theCursor = self.textCursor() + theCursor.beginEditBlock() + theCursor.insertText(nwUnicode.U_NBSP) + theCursor.endEditBlock() + return + def _openSpellContext(self): self._openContextMenu(self.cursorRect().center()) return @@ -442,15 +473,15 @@ class GuiDocEditor(QTextEdit): elif self.mainConf.doReplaceDash and theTwo == "--": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) - theCursor.insertText("\u2013") + theCursor.insertText(nwUnicode.U_ENDASH) - elif self.mainConf.doReplaceDash and theTwo == "\u2013-": + elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH+"-": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) - theCursor.insertText("\u2014") + theCursor.insertText(nwUnicode.U_EMDASH) elif self.mainConf.doReplaceDots and theThree == "...": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3) - theCursor.insertText("\u2026") + theCursor.insertText(nwUnicode.U_HELLIP) return diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 74506f71..e356dbf7 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -16,6 +16,8 @@ import nw from PyQt5.QtCore import Qt, QRegularExpression from PyQt5.QtGui import QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush +from nw.constants import nwUnicode + logger = logging.getLogger(__name__) class GuiDocHighlighter(QSyntaxHighlighter): @@ -87,6 +89,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): "strike" : self._makeFormat(self.colEmph, "strike"), "underline" : self._makeFormat(self.colEmph, "underline"), "trailing" : self._makeFormat(self.colTrail,"background"), + "nobreak" : self._makeFormat(self.colTrail,"background"), "dialogue1" : self._makeFormat(self.colDialN), "dialogue2" : self._makeFormat(self.colDialD), "dialogue3" : self._makeFormat(self.colDialS), @@ -145,6 +148,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): } )) + # Non-breaking Space + self.hRules.append(( + "[\u00a0]+", { + 0 : self.hStyles["nobreak"], + } + )) + # Markdown self.hRules.append(( r"(?, and this C with . While is just . - +Let’s also add some text with a non-breaking space in it, like right here! diff --git a/sample/sampleNovel/data_b/a8a28a246524_main.nwd b/sample/sampleNovel/data_b/a8a28a246524_main.nwd index 24790377..fd3199ca 100644 --- a/sample/sampleNovel/data_b/a8a28a246524_main.nwd +++ b/sample/sampleNovel/data_b/a8a28a246524_main.nwd @@ -13,6 +13,3 @@ With many cheerful facts about the square of the hypotenuse With many cheerful facts about the square of the hypotenuse With many cheerful facts about the square of the hypotenuse With many cheerful facts about the square of the hypotepotenuse - - - diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 1412184e..c551ad8d 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -11,7 +11,7 @@ True ba8a28a246524 ba8a28a246524 - 859 + 873 B E @@ -79,10 +79,10 @@ Notes False SCENE - 713 - 132 - 6 - 72 + 787 + 146 + 7 + 152 Another Scene @@ -103,10 +103,10 @@ Finished False UNNUMBERED - 626 + 630 101 3 - 583 + 648 A Note on Ipsums