From 146756a3957774c5aba36443e0ebf01beedfcdfa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Oct 2019 11:35:40 +0100 Subject: [PATCH 1/3] Added unicode constants class, and support for non-breaking spaces, which aren't preserved on save. --- nw/config.py | 6 +- nw/constants.py | 80 ++++++++++++++----- nw/gui/doceditor.py | 36 +++++++-- nw/gui/dochighlight.py | 10 +++ .../sampleNovel/data_b/a8a28a246524_main.nwd | 2 + sample/sampleNovel/nwProject.nwx | 12 +-- 6 files changed, 111 insertions(+), 35 deletions(-) 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..e06942c8 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -142,26 +142,66 @@ class nwQuotes(): "\u300e", # Left white corner bracket "\u300f", # Right white corner bracket ] + +# END Class nwQuotes + +class nwUnicode: + """Suppoted unicode character constants and translation maps. + """ + + # 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_NDASH = "\u2013" # Short dash + U_MDASH = "\u2014" # Long dash + U_HELLIP = "\u2026" # Ellipsis + + # Other + U_NBSP = "\u00a0" # Non-breaking space + HTML = { - "\u0022" : """, - "\u0027" : "'", - "\u00ab" : "«", - "\u00bb" : "»", - "\u2018" : "‘", - "\u2019" : "’", - "\u201a" : "‚", - "\u201b" : "‛", - "\u201c" : "“", - "\u201d" : "”", - "\u201e" : "„", - "\u201f" : "‟", - "\u2039" : "‹", - "\u203a" : "›", - "\u2e42" : "⹂", - "\u300c" : "「", - "\u300d" : "」", - "\u300e" : "『", - "\u300f" : "』", + U_QUOT : """, + U_APOS : "'", + U_LAQUO : "«", + U_RAQUO : "»", + U_LSQUO : "‘", + U_RSQUO : "’", + U_SBQUO : "‚", + U_SUQUO : "‛", + U_LDQUO : "“", + U_RDQUO : "”", + U_BDQUO : "„", + U_UDQUO : "‟", + U_LSAQUO : "‹", + U_RSAQUO : "›", + U_BDRQUO : "⹂", + U_LCQUO : "「", + U_RCQUO : "」", + U_LWCQUO : "『", + U_LWCQUO : "『", + U_NDASH : "–", + U_MDASH : "—", + U_HELLIP : "…", + U_NBSP : " ", } -# END Class nwQuotes \ No newline at end of file +# END Class nwUnicode diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 03d3313d..e2ca9237 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__) @@ -316,9 +316,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 +349,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 +466,15 @@ class GuiDocEditor(QTextEdit): elif self.mainConf.doReplaceDash and theTwo == "--": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) - theCursor.insertText("\u2013") + theCursor.insertText(nwUnicode.U_NDASH) - elif self.mainConf.doReplaceDash and theTwo == "\u2013-": + elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_NDASH+"-": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) - theCursor.insertText("\u2014") + theCursor.insertText(nwUnicode.U_MDASH) 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"(? - + Sample Project Sample Project @@ -11,7 +11,7 @@ True ba8a28a246524 ba8a28a246524 - 859 + 864 B E @@ -103,10 +103,10 @@ Finished False UNNUMBERED - 626 - 101 - 3 - 583 + 658 + 106 + 4 + 678 A Note on Ipsums From 489eae2e0d2deeb03f19d76254e21d22adcb6ee3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Oct 2019 14:49:11 +0100 Subject: [PATCH 2/3] Adding non-breaking space support for editor and html, latex export --- nw/constants.py | 68 +++++++++++++++++++++----------------- nw/convert/file/html.py | 25 +++++++------- nw/convert/file/text.py | 1 + nw/convert/text/tohtml.py | 15 +++++---- nw/convert/text/tolatex.py | 15 +++++++++ nw/convert/text/totext.py | 12 +++++++ nw/convert/tokenizer.py | 3 ++ nw/gui/doceditor.py | 15 ++++++--- nw/gui/docviewer.py | 1 + nw/project/document.py | 3 +- 10 files changed, 104 insertions(+), 54 deletions(-) diff --git a/nw/constants.py b/nw/constants.py index e06942c8..92c693dc 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -149,7 +149,9 @@ class nwUnicode: """Suppoted unicode character constants and translation maps. """ - # Quotation Marks + # Unicode Constants + + ## Quotation Marks U_QUOT = "\u0022" # Quotation mark U_APOS = "\u0027" # Apostrophe U_LAQUO = "\u00ab" # Left-pointing double angle quotation mark @@ -170,38 +172,44 @@ class nwUnicode: U_LWCQUO = "\u300e" # Left white corner bracket U_RECQUO = "\u300f" # Right white corner bracket - # Punctuation - U_NDASH = "\u2013" # Short dash - U_MDASH = "\u2014" # Long dash + ## Punctuation + U_ENDASH = "\u2013" # Short dash + U_EMDASH = "\u2014" # Long dash U_HELLIP = "\u2026" # Ellipsis - # Other + ## Other U_NBSP = "\u00a0" # Non-breaking space + U_PARA = "\u2029" # Paragraph separator - HTML = { - U_QUOT : """, - U_APOS : "'", - U_LAQUO : "«", - U_RAQUO : "»", - U_LSQUO : "‘", - U_RSQUO : "’", - U_SBQUO : "‚", - U_SUQUO : "‛", - U_LDQUO : "“", - U_RDQUO : "”", - U_BDQUO : "„", - U_UDQUO : "‟", - U_LSAQUO : "‹", - U_RSAQUO : "›", - U_BDRQUO : "⹂", - U_LCQUO : "「", - U_RCQUO : "」", - U_LWCQUO : "『", - U_LWCQUO : "『", - U_NDASH : "–", - U_MDASH : "—", - U_HELLIP : "…", - U_NBSP : " ", - } + # 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 e2ca9237..1d0c35ac 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -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): @@ -466,11 +473,11 @@ class GuiDocEditor(QTextEdit): elif self.mainConf.doReplaceDash and theTwo == "--": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) - theCursor.insertText(nwUnicode.U_NDASH) + theCursor.insertText(nwUnicode.U_ENDASH) - elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_NDASH+"-": + elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH+"-": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) - theCursor.insertText(nwUnicode.U_MDASH) + theCursor.insertText(nwUnicode.U_EMDASH) elif self.mainConf.doReplaceDots and theThree == "...": theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 72c85ee4..76c57e67 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -106,6 +106,7 @@ class GuiDocViewer(QTextBrowser): aDoc.doAutoReplace() aDoc.tokenizeText() aDoc.doConvert() + aDoc.doPostProcessing() self.setHtml(aDoc.theResult) if self.theHandle == tHandle: self.verticalScrollBar().setValue(sPos) diff --git a/nw/project/document.py b/nw/project/document.py index 7f5b550a..923f5c9e 100644 --- a/nw/project/document.py +++ b/nw/project/document.py @@ -100,7 +100,8 @@ class NWDoc(): self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) return False - if path.isfile(docTemp): unlink(docTemp) + if path.isfile(docTemp): + unlink(docTemp) self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName) From bbe1d2960c50f185e8ed9bf496afbf3e5b3a7ff8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Oct 2019 14:49:48 +0100 Subject: [PATCH 3/3] Added example of non-breaking space in sample project --- .../sampleNovel/data_6/36b6aa9b697b_main.nwd | 2 +- .../sampleNovel/data_b/a8a28a246524_main.nwd | 5 ----- sample/sampleNovel/nwProject.nwx | 20 +++++++++---------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index 5b15e42b..4b2e8607 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -18,4 +18,4 @@ This one is a bit longer. “It also has some dialogue in it” she said, before Let’s auto-replace this A with , 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 413a5baf..fd3199ca 100644 --- a/sample/sampleNovel/data_b/a8a28a246524_main.nwd +++ b/sample/sampleNovel/data_b/a8a28a246524_main.nwd @@ -13,8 +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 - -Typing Very Fast — too fast! - - - diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 1b33734a..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 - 864 + 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 - 658 - 106 - 4 - 678 + 630 + 101 + 3 + 648 A Note on Ipsums