diff --git a/README.md b/README.md
index af613ee4..2af3f52b 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,6 @@ of writing novels. The formatting is currently limited to:
* Headings levels 1 to 4 using the `#` syntax only.
* Emphasised and strongly emphasised text. These are rendered as italicised and bold text.
* Strikethrough text.
-* Hard line breaks using two or more spaces at the end of a line.
That is it. Features not supported in the editor are also not exported when using the export tool.
diff --git a/docs/source/int_introduction.rst b/docs/source/int_introduction.rst
index 900c3921..693b8dbe 100644
--- a/docs/source/int_introduction.rst
+++ b/docs/source/int_introduction.rst
@@ -38,8 +38,7 @@ at the same time provide a complete set of features needed for writing a novel.
.. tip::
If you do need to align information in rows and columns in your notes, you can achieve this with
- tabs and hard line breaks. The tab stop width can be specified in :guilabel:`Preferences` and
- hard line breaks can be inserted by adding to spaces at the end of the line.
+ tabs and line breaks. The tab stop width can be specified in :guilabel:`Preferences`.
The main window does not have a toolbar like many other applications do. This reduces clutter, and
since the documents are formatted with markdown tags, is more or less redundant. However, all
diff --git a/docs/source/usage_interface.rst b/docs/source/usage_interface.rst
index 47038ae9..7c4f92ee 100644
--- a/docs/source/usage_interface.rst
+++ b/docs/source/usage_interface.rst
@@ -205,15 +205,16 @@ tricky for languages that use the same symbol for these.
.. _a_ui_md:
-Markdown Format
-===============
+The Markdown-Like Format
+========================
-The document editor uses a simplified markdown format. That is, it supports basic formatting like
-emphasis (italic), strong importance (bold) and strikethrough text, as well as four levels of
-headings.
+The editor itself is a plaintext editor that uses formatting codes for setting meta data values and
+allowing for some text formatting. The syntax is based on Markdown, but novelWriter is *not* a
+Markdown editor. It supports basic formatting like emphasis (italic), strong importance (bold)
+and strikethrough text, as well as four levels of headings.
-Some non-standard markdown features have been added. For instance, novelWriter allows for comments,
-a synopsis tag, and a set of keyword and value sets used for tags and references.
+In addition to formatting codes, novelWriter allows for comments, a synopsis tag, and a set of
+keyword and value sets used for tags and references.
.. _a_ui_md_head:
@@ -253,6 +254,33 @@ level of the novel. See :ref:`a_struct_heads` for more details.
itself. See :ref:`a_struct_heads_unnum` for more details.
+.. _a_ui_md_text:
+
+Text Paragraphs
+---------------
+
+A text paragraph is indicated by a blank line. That is, you need two line breaks to separate two
+fragments of text into two paragraphs. Single line breaks are treated as line breaks within a
+paragraph.
+
+In addition, the editor supports a few additional types of whitespaces.
+
+* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
+* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
+ :kbd:`Shift`:kbd:`Space`.
+* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
+
+These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed
+in :ref:`a_ui_shortcuts_ins`.
+
+Non-breaking spaces are highlighted by the syntax highlighter with an alternate coloured
+background, depending on the selected theme.
+
+.. tip::
+ Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
+ it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
+
+
.. _a_ui_md_emph:
Text Emphasis
@@ -333,34 +361,6 @@ The available tag and reference keywords are listed in the :ref:`a_struct_tags`
also be inserted at the cursor position in the editor via the :guilabel:`Insert` menu.
-.. _a_ui_md_add:
-
-Additional Markdown and Non-Standard Features
----------------------------------------------
-
-The editor and viewer also support markdown standard hard line breaks, and preserve non-breaking
-spaces if running with Qt 5.9 or higher. For older versions, the non-breaking spaces are lost when
-the document is saved. This is unfortunately hard-coded into the Qt text editor.
-
-* A hard line break can be achieved by leaving two or more spaces at the end of the line. This is
- standard markdown syntax. Alternatively, the user can press :kbd:`Ctrl`:kbd:`K`, :kbd:`Return` to
- insert this type of line break.
-* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
-* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
- :kbd:`Shift`:kbd:`Space`.
-* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
-
-These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed
-in :ref:`a_ui_shortcuts_ins`.
-
-Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an
-alternate coloured background, depending on the selected theme.
-
-.. tip::
- Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
- it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
-
-
.. _a_ui_outline:
Project Outline View
@@ -514,7 +514,6 @@ a key or key combination for the inserted content.
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime."
- ":kbd:`Ctrl`:kbd:`K`, :kbd:`Return`", "Insert a hard line break."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space."
diff --git a/nw/assets/themes/syntax/default_dark.conf b/nw/assets/themes/syntax/default_dark.conf
index 09b292ee..f015e5d6 100644
--- a/nw/assets/themes/syntax/default_dark.conf
+++ b/nw/assets/themes/syntax/default_dark.conf
@@ -20,6 +20,6 @@ hidden = 150, 150, 150
keyword = 200, 46, 0
value = 184, 200, 0
spellcheckline = 200, 46, 0
-tagerror = 46, 200, 0
+errorline = 46, 200, 0
replacetag = 0, 184, 46
modifier = 200, 120, 0
diff --git a/nw/assets/themes/syntax/default_light.conf b/nw/assets/themes/syntax/default_light.conf
index 908fd7cb..319fbda3 100644
--- a/nw/assets/themes/syntax/default_light.conf
+++ b/nw/assets/themes/syntax/default_light.conf
@@ -20,6 +20,6 @@ hidden = 100, 100, 100
keyword = 200, 50, 50
value = 50, 150, 50
spellcheckline = 200, 0, 0
-tagerror = 0, 150, 0
+errorline = 0, 150, 0
replacetag = 0, 150, 0
modifier = 150, 110, 30
diff --git a/nw/assets/themes/syntax/grey_dark.conf b/nw/assets/themes/syntax/grey_dark.conf
index a6bc0420..e579e8da 100644
--- a/nw/assets/themes/syntax/grey_dark.conf
+++ b/nw/assets/themes/syntax/grey_dark.conf
@@ -20,6 +20,6 @@ hidden = 150, 150, 150
keyword = 225, 225, 225
value = 200, 200, 200
spellcheckline = 200, 46, 0
-tagerror = 46, 200, 0
+errorline = 46, 200, 0
replacetag = 225, 225, 225
modifier = 225, 225, 225
diff --git a/nw/assets/themes/syntax/grey_light.conf b/nw/assets/themes/syntax/grey_light.conf
index a44593b4..32f9561c 100644
--- a/nw/assets/themes/syntax/grey_light.conf
+++ b/nw/assets/themes/syntax/grey_light.conf
@@ -20,6 +20,6 @@ hidden = 100, 100, 100
keyword = 0, 0, 0
value = 20, 20, 20
spellcheckline = 200, 0, 0
-tagerror = 0, 150, 0
+errorline = 0, 150, 0
replacetag = 0, 0, 0
modifier = 0, 0, 0
diff --git a/nw/assets/themes/syntax/light_owl.conf b/nw/assets/themes/syntax/light_owl.conf
index fde36665..fdd0dc67 100644
--- a/nw/assets/themes/syntax/light_owl.conf
+++ b/nw/assets/themes/syntax/light_owl.conf
@@ -40,6 +40,6 @@ hidden = 152, 159, 177
keyword = 222, 61, 58
value = 150, 74, 193
spellcheckline = 222, 61, 58
-tagerror = 8, 145, 106
+errorline = 8, 145, 106
replacetag = 42, 162, 152
modifier = 224, 175, 5
diff --git a/nw/assets/themes/syntax/night_owl.conf b/nw/assets/themes/syntax/night_owl.conf
index 56cebf53..59ad10da 100644
--- a/nw/assets/themes/syntax/night_owl.conf
+++ b/nw/assets/themes/syntax/night_owl.conf
@@ -40,6 +40,6 @@ hidden = 99, 119, 119
keyword = 247, 140, 108
value = 199, 146, 234
spellcheckline = 247, 140, 108
-tagerror = 173, 219, 103
+errorline = 173, 219, 103
replacetag = 127, 219, 202
modifier = 236, 196, 141
diff --git a/nw/assets/themes/syntax/solarized_dark.conf b/nw/assets/themes/syntax/solarized_dark.conf
index 965430bb..ec8a3d8d 100644
--- a/nw/assets/themes/syntax/solarized_dark.conf
+++ b/nw/assets/themes/syntax/solarized_dark.conf
@@ -20,6 +20,6 @@ hidden = 147, 161, 161
keyword = 133, 153, 0
value = 203, 75, 22
spellcheckline = 203, 75, 22
-tagerror = 220, 50, 47
+errorline = 220, 50, 47
replacetag = 133, 153, 0
modifier = 181, 137, 0
diff --git a/nw/assets/themes/syntax/solarized_light.conf b/nw/assets/themes/syntax/solarized_light.conf
index 2d1f6d1c..319a084c 100644
--- a/nw/assets/themes/syntax/solarized_light.conf
+++ b/nw/assets/themes/syntax/solarized_light.conf
@@ -20,6 +20,6 @@ hidden = 88, 110, 117
keyword = 133, 153, 0
value = 203, 75, 22
spellcheckline = 203, 75, 22
-tagerror = 220, 50, 47
+errorline = 220, 50, 47
replacetag = 133, 153, 0
modifier = 181, 137, 0
diff --git a/nw/assets/themes/syntax/tomorrow.conf b/nw/assets/themes/syntax/tomorrow.conf
index 85fe84ea..6333f06e 100644
--- a/nw/assets/themes/syntax/tomorrow.conf
+++ b/nw/assets/themes/syntax/tomorrow.conf
@@ -40,6 +40,6 @@ hidden = 142, 144, 140
keyword = 240, 40, 41
value = 137, 89, 168
spellcheckline = 240, 40, 41
-tagerror = 113, 140, 0
+errorline = 113, 140, 0
replacetag = 62, 153, 159
modifier = 245, 135, 31
diff --git a/nw/assets/themes/syntax/tomorrow_night.conf b/nw/assets/themes/syntax/tomorrow_night.conf
index be8f2d4d..2a4e8751 100644
--- a/nw/assets/themes/syntax/tomorrow_night.conf
+++ b/nw/assets/themes/syntax/tomorrow_night.conf
@@ -40,6 +40,6 @@ hidden = 150, 152, 150
keyword = 204, 102, 102
value = 178, 148, 187
spellcheckline = 204, 102, 102
-tagerror = 181, 189, 104
+errorline = 181, 189, 104
replacetag = 138, 190, 183
modifier = 222, 147, 95
diff --git a/nw/assets/themes/syntax/tomorrow_night_blue.conf b/nw/assets/themes/syntax/tomorrow_night_blue.conf
index 3738051f..1a8459bf 100644
--- a/nw/assets/themes/syntax/tomorrow_night_blue.conf
+++ b/nw/assets/themes/syntax/tomorrow_night_blue.conf
@@ -40,6 +40,6 @@ hidden = 114, 133, 183
keyword = 255, 157, 164
value = 235, 187, 255
spellcheckline = 255, 157, 164
-tagerror = 209, 241, 169
+errorline = 209, 241, 169
replacetag = 153, 255, 255
modifier = 255, 197, 143
diff --git a/nw/assets/themes/syntax/tomorrow_night_bright.conf b/nw/assets/themes/syntax/tomorrow_night_bright.conf
index 9ce48b6a..c5bccc99 100644
--- a/nw/assets/themes/syntax/tomorrow_night_bright.conf
+++ b/nw/assets/themes/syntax/tomorrow_night_bright.conf
@@ -40,6 +40,6 @@ hidden = 150, 152, 150
keyword = 213, 78, 83
value = 195, 151, 216
spellcheckline = 213, 78, 83
-tagerror = 185, 202, 74
+errorline = 185, 202, 74
replacetag = 112, 192, 177
modifier = 231, 140, 69
diff --git a/nw/assets/themes/syntax/tomorrow_night_eighties.conf b/nw/assets/themes/syntax/tomorrow_night_eighties.conf
index 9d611cd3..c7314f17 100644
--- a/nw/assets/themes/syntax/tomorrow_night_eighties.conf
+++ b/nw/assets/themes/syntax/tomorrow_night_eighties.conf
@@ -40,6 +40,6 @@ hidden = 153, 153, 153
keyword = 242, 119, 122
value = 204, 153, 204
spellcheckline = 242, 119, 122
-tagerror = 153, 204, 153
+errorline = 153, 204, 153
replacetag = 102, 204, 204
modifier = 249, 145, 57
diff --git a/nw/config.py b/nw/config.py
index 65436608..16647abd 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -138,6 +138,7 @@ class Config:
self.doJustify = False # Justify text
self.showTabsNSpaces = False # Show tabs and spaces in edior
self.showLineEndings = False # Show line endings in editor
+ self.showMultiSpaces = True # Highlight multiple spaces in the text
self.doReplace = True # Enable auto-replace as you type
self.doReplaceSQuote = True # Smart single quotes
@@ -602,6 +603,9 @@ class Config:
self.showLineEndings = self._parseLine(
cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings
)
+ self.showMultiSpaces = self._parseLine(
+ cnfParse, cnfSec, "showmultispaces", self.CNF_BOOL, self.showMultiSpaces
+ )
self.bigDocLimit = self._parseLine(
cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit
)
@@ -765,6 +769,7 @@ class Config:
cnfParse.set(cnfSec, "spellcheck", str(self.spellLanguage))
cnfParse.set(cnfSec, "showtabsnspaces", str(self.showTabsNSpaces))
cnfParse.set(cnfSec, "showlineendings", str(self.showLineEndings))
+ cnfParse.set(cnfSec, "showmultispaces", str(self.showMultiSpaces))
cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit))
cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath))
cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes))
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 180c2da8..5586575d 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -151,7 +151,6 @@ class ToHtml(Tokenizer):
thisPar = []
parStyle = None
tmpResult = []
- hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
@@ -196,16 +195,15 @@ class ToHtml(Tokenizer):
if tType == self.T_EMPTY:
if parStyle is None:
parStyle = ""
- if hasHardBreak and self.cssStyles:
+ if len(thisPar) > 1 and self.cssStyles:
parClass = " class='break'"
else:
parClass = ""
if len(thisPar) > 0:
- tTemp = "".join(thisPar)
+ tTemp = "
".join(thisPar)
tmpResult.append("
%s
\n" % (parStyle, parClass, tTemp.rstrip()))
thisPar = []
parStyle = None
- hasHardBreak = False
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "
")
@@ -239,11 +237,7 @@ class ToHtml(Tokenizer):
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
- if tText.endswith(" "):
- thisPar.append(tTemp.rstrip() + "
")
- hasHardBreak = True
- else:
- thisPar.append(tTemp.rstrip() + " ")
+ thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
diff --git a/nw/core/tomd.py b/nw/core/tomd.py
index 684a23fb..d95a6810 100644
--- a/nw/core/tomd.py
+++ b/nw/core/tomd.py
@@ -89,12 +89,12 @@ class ToMarkdown(Tokenizer):
thisPar = []
tmpResult = []
- for tType, tLine, tText, tFormat, tStyle in self.theTokens:
+ for tType, _, tText, tFormat, tStyle in self.theTokens:
# Process Text Type
if tType == self.T_EMPTY:
if len(thisPar) > 0:
- tTemp = "".join(thisPar)
+ tTemp = " \n".join(thisPar)
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
thisPar = []
@@ -128,10 +128,7 @@ class ToMarkdown(Tokenizer):
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
- if tText.endswith(" "):
- thisPar.append(tTemp.rstrip() + " \n")
- else:
- thisPar.append(tTemp.rstrip() + " ")
+ thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
diff --git a/nw/core/toodt.py b/nw/core/toodt.py
index ce67e8c0..f5770c9f 100644
--- a/nw/core/toodt.py
+++ b/nw/core/toodt.py
@@ -327,8 +327,7 @@ class ToOdt(Tokenizer):
thisPar = []
thisFmt = []
parStyle = None
- hasHardBreak = False
- for tType, tLine, tText, tFormat, tStyle in self.theTokens:
+ for tType, _, tText, tFormat, tStyle in self.theTokens:
# Styles
oStyle = ODTParagraphStyle()
@@ -359,13 +358,13 @@ class ToOdt(Tokenizer):
# Process Text Types
if tType == self.T_EMPTY:
- if hasHardBreak and parStyle is not None:
+ if len(thisPar) > 1 and parStyle is not None:
if self.doJustify:
parStyle.setTextAlign("left")
if len(thisPar) > 0:
- tTemp = "".join(thisPar)
- fTemp = "".join(thisFmt)
+ tTemp = "\n".join(thisPar)
+ fTemp = " ".join(thisFmt)
tTxt = tTemp.rstrip()
tFmt = fTemp[:len(tTxt)]
self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
@@ -373,7 +372,6 @@ class ToOdt(Tokenizer):
thisPar = []
thisFmt = []
parStyle = None
- hasHardBreak = False
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
@@ -402,23 +400,17 @@ class ToOdt(Tokenizer):
self._addTextPar("Text_Body", oStyle, "")
elif tType == self.T_TEXT:
- tTemp = tText
if parStyle is None:
parStyle = oStyle
- tFmt = " "*len(tTemp)
+ tFmt = " "*len(tText)
for xPos, xLen, xFmt in tFormat:
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
- tTxt = tTemp.rstrip()
+ tTxt = tText.rstrip()
tFmt = tFmt[:len(tTxt)]
- if tText.endswith(" "):
- thisPar.append(tTxt + "\n")
- thisFmt.append(tFmt + " ")
- hasHardBreak = True
- else:
- thisPar.append(tTxt + " ")
- thisFmt.append(tFmt + " ")
+ thisPar.append(tTxt)
+ thisFmt.append(tFmt)
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
diff --git a/nw/dialogs/preferences.py b/nw/dialogs/preferences.py
index 5ed23996..2680dc64 100644
--- a/nw/dialogs/preferences.py
+++ b/nw/dialogs/preferences.py
@@ -924,6 +924,19 @@ class GuiPreferencesSyntax(QWidget):
self.tr("Applies to emphasis (italic) and strong (bold).")
)
+ # Text Errors
+ # ===========
+
+ self.mainForm.addGroupLabel(self.tr("Text Errors"))
+
+ self.showMultiSpaces = QSwitch()
+ self.showMultiSpaces.setChecked(self.mainConf.showMultiSpaces)
+ self.mainForm.addRow(
+ self.tr("Mark redundant spaces"),
+ self.showMultiSpaces,
+ self.tr("Trailing spaces or multiple spaces between words.")
+ )
+
return
def saveValues(self):
@@ -940,6 +953,9 @@ class GuiPreferencesSyntax(QWidget):
# Text Emphasis
self.mainConf.highlightEmph = self.highlightEmph.isChecked()
+ # Text Errors
+ self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked()
+
self.mainConf.confChanged = True
return
diff --git a/nw/enum.py b/nw/enum.py
index 7b3c8aaa..830e8953 100644
--- a/nw/enum.py
+++ b/nw/enum.py
@@ -89,17 +89,17 @@ class nwDocAction(Enum):
BLOCK_TXT = 18
REPL_SNG = 19
REPL_DBL = 20
+ RM_BREAKS = 21
# END Enum nwDocAction
class nwDocInsert(Enum):
NO_INSERT = 0
- HARD_BREAK = 1
- QUOTE_LS = 2
- QUOTE_RS = 3
- QUOTE_LD = 4
- QUOTE_RD = 5
+ QUOTE_LS = 1
+ QUOTE_RS = 2
+ QUOTE_LD = 3
+ QUOTE_RD = 4
# END Enum nwDocInsert
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index 9095dea0..a6cc404e 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -791,6 +791,8 @@ class GuiDocEditor(QTextEdit):
self._replaceQuotes("'", self._typSQOpen, self._typSQClose)
elif theAction == nwDocAction.REPL_DBL:
self._replaceQuotes("\"", self._typDQOpen, self._typDQClose)
+ elif theAction == nwDocAction.RM_BREAKS:
+ self._removeInParLineBreaks()
else:
logger.debug("Unknown or unsupported document action %s" % str(theAction))
self._allowAutoReplace(True)
@@ -840,9 +842,7 @@ class GuiDocEditor(QTextEdit):
if isinstance(theInsert, str):
theText = theInsert
elif isinstance(theInsert, nwDocInsert):
- if theInsert == nwDocInsert.HARD_BREAK:
- theText = " \n"
- elif theInsert == nwDocInsert.QUOTE_LS:
+ if theInsert == nwDocInsert.QUOTE_LS:
theText = self._typSQOpen
elif theInsert == nwDocInsert.QUOTE_RS:
theText = self._typSQClose
@@ -1787,6 +1787,54 @@ class GuiDocEditor(QTextEdit):
return True
+ def _removeInParLineBreaks(self):
+ """Strip line breaks within paragraphs in the selected text.
+ """
+ theCursor = self.textCursor()
+ theDoc = self.document()
+
+ iS = 0
+ iE = theDoc.blockCount() - 1
+ rS = 0
+ rE = theDoc.characterCount()
+ if theCursor.hasSelection():
+ sBlock = theDoc.findBlock(theCursor.selectionStart())
+ eBlock = theDoc.findBlock(theCursor.selectionEnd())
+ iS = sBlock.blockNumber()
+ iE = eBlock.blockNumber()
+ rS = sBlock.position()
+ rE = eBlock.position() + eBlock.length()
+
+ # Clean up the text
+ currPar = []
+ cleanText = ""
+ for i in range(iS, iE+1):
+ cBlock = theDoc.findBlockByNumber(i)
+ cText = cBlock.text()
+ if cText.strip() == "":
+ if currPar:
+ cleanText += " ".join(currPar) + "\n\n"
+ else:
+ cleanText += "\n"
+ currPar = []
+ elif cText.startswith(("# ", "## ", "### ", "#### ", "@", "%")):
+ cleanText += cText + "\n"
+ else:
+ currPar.append(cText)
+
+ if currPar:
+ cleanText += " ".join(currPar) + "\n\n"
+
+ # Replace the text with the cleaned up text
+ theCursor.beginEditBlock()
+ theCursor.clearSelection()
+ theCursor.setPosition(rS)
+ theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
+ theCursor.insertText(cleanText.rstrip() + "\n")
+ theCursor.endEditBlock()
+
+ return True
+
def _makeSelection(self, selMode):
"""Wrapper function to select text based on a selection mode.
"""
diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py
index ea9df173..ceeacf45 100644
--- a/nw/gui/dochighlight.py
+++ b/nw/gui/dochighlight.py
@@ -71,7 +71,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colKey = QColor(0, 0, 0)
self.colVal = QColor(0, 0, 0)
self.colSpell = QColor(0, 0, 0)
- self.colTagErr = QColor(0, 0, 0)
+ self.colError = QColor(0, 0, 0)
self.colRepTag = QColor(0, 0, 0)
self.initHighlighter()
@@ -95,11 +95,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colKey = QColor(*self.theTheme.colKey)
self.colVal = QColor(*self.theTheme.colVal)
self.colSpell = QColor(*self.theTheme.colSpell)
- self.colTagErr = QColor(*self.theTheme.colTagErr)
+ self.colError = QColor(*self.theTheme.colError)
self.colRepTag = QColor(*self.theTheme.colRepTag)
self.colMod = QColor(*self.theTheme.colMod)
- self.colTrail = QColor(*self.theTheme.colEmph)
- self.colTrail.setAlpha(64)
+ self.colBreak = QColor(*self.theTheme.colEmph)
+ self.colBreak.setAlpha(64)
self.colEmph = None
if self.mainConf.highlightEmph:
@@ -117,8 +117,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"bold" : self._makeFormat(self.colEmph, "bold"),
"italic" : self._makeFormat(self.colEmph, "italic"),
"strike" : self._makeFormat(self.colHidden, "strike"),
- "trailing" : self._makeFormat(self.colTrail, "background"),
- "nobreak" : self._makeFormat(self.colTrail, "background"),
+ "mspaces" : self._makeFormat(self.colError, "errline"),
+ "nobreak" : self._makeFormat(self.colBreak, "background"),
"dialogue1" : self._makeFormat(self.colDialN),
"dialogue2" : self._makeFormat(self.colDialD),
"dialogue3" : self._makeFormat(self.colDialS),
@@ -131,12 +131,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.hRules = []
- # Trailing Spaces, 2+
- self.hRules.append((
- r"[ ]{2,}$", {
- 0 : self.hStyles["trailing"],
- }
- ))
+ # Multiple or Trailing Spaces
+ if self.mainConf.showMultiSpaces:
+ self.hRules.append((
+ r"[ ]{2,}|[ ]*$", {
+ 0 : self.hStyles["mspaces"],
+ }
+ ))
# Non-Breaking Spaces
self.hRules.append((
@@ -174,7 +175,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
}
))
- # Markdown
+ # Markdown Syntax
self.hRules.append((
nwRegEx.FMT_EI, {
1 : self.hStyles["hidden"],
@@ -296,7 +297,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(xPos, xLen, self.hStyles["value"])
else:
kwFmt = self.format(xPos)
- kwFmt.setUnderlineColor(self.colTagErr)
+ kwFmt.setUnderlineColor(self.colError)
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(xPos, xLen, kwFmt)
@@ -391,6 +392,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
theFormat.setFontItalic(True)
if "strike" in fmtStyle:
theFormat.setFontStrikeOut(True)
+ if "errline" in fmtStyle:
+ theFormat.setUnderlineColor(self.colError)
+ theFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
if "underline" in fmtStyle:
theFormat.setFontUnderline(True)
if "background" in fmtStyle:
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 402e40a9..d259c6cd 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -634,15 +634,8 @@ class GuiMainMenu(QMenuBar):
self.aInsDPrime.triggered.connect(lambda: self._docInsert(nwUnicode.U_DPRIME))
self.mInsPunct.addAction(self.aInsDPrime)
- # Insert > Breaks and Spaces
- self.mInsBreaks = self.insertMenu.addMenu(self.tr("Breaks and Spaces"))
-
- # Insert > Hard Line Break
- self.aInsHardBreak = QAction(self.tr("Hard Line Break"), self)
- self.aInsHardBreak.setStatusTip(self.tr("Insert a hard line break"))
- self.aInsHardBreak.setShortcut("Ctrl+K, Return")
- self.aInsHardBreak.triggered.connect(lambda: self._docInsert(nwDocInsert.HARD_BREAK))
- self.mInsBreaks.addAction(self.aInsHardBreak)
+ # Insert > White Spaces
+ self.mInsBreaks = self.insertMenu.addMenu(self.tr("White Spaces"))
# Insert > Non-Breaking Space
self.aInsNBSpace = QAction(self.tr("Non-Breaking Space"), self)
@@ -750,60 +743,6 @@ class GuiMainMenu(QMenuBar):
return
- def _buildSearchMenu(self):
- """Assemble the Search menu.
- """
- # Search
- self.srcMenu = self.addMenu(self.tr("&Search"))
-
- # Search > Find
- self.aFind = QAction(self.tr("Find"), self)
- self.aFind.setStatusTip(self.tr("Find text in document"))
- self.aFind.setShortcut("Ctrl+F")
- self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch())
- self.srcMenu.addAction(self.aFind)
-
- # Search > Replace
- self.aReplace = QAction(self.tr("Replace"), self)
- self.aReplace.setStatusTip(self.tr("Replace text in document"))
- if self.mainConf.osDarwin:
- self.aReplace.setShortcut("Ctrl+=")
- else:
- self.aReplace.setShortcut("Ctrl+H")
- self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace())
- self.srcMenu.addAction(self.aReplace)
-
- # Search > Find Next
- self.aFindNext = QAction(self.tr("Find Next"), self)
- self.aFindNext.setStatusTip(self.tr("Find next occurrence of text in document"))
- if self.mainConf.osDarwin:
- self.aFindNext.setShortcuts(["Ctrl+G", "F3"])
- else:
- self.aFindNext.setShortcuts(["F3", "Ctrl+G"])
- self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext())
- self.srcMenu.addAction(self.aFindNext)
-
- # Search > Find Prev
- self.aFindPrev = QAction(self.tr("Find Previous"), self)
- self.aFindPrev.setStatusTip(self.tr("Find previous occurrence of text in document"))
- if self.mainConf.osDarwin:
- self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"])
- else:
- self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"])
- self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True))
- self.srcMenu.addAction(self.aFindPrev)
-
- # Search > Replace Next
- self.aReplaceNext = QAction(self.tr("Replace Next"), self)
- self.aReplaceNext.setStatusTip(
- self.tr("Find and replace next occurrence of text in document")
- )
- self.aReplaceNext.setShortcut("Ctrl+Shift+1")
- self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext())
- self.srcMenu.addAction(self.aReplaceNext)
-
- return
-
def _buildFormatMenu(self):
"""Assemble the Format menu.
"""
@@ -912,6 +851,68 @@ class GuiMainMenu(QMenuBar):
self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL))
self.fmtMenu.addAction(self.aFmtReplDbl)
+ # Format > Remove In-Paragraph Breaks
+ self.aFmtRmBreaks = QAction(self.tr("Remove In-Paragraph Breaks"), self)
+ self.aFmtRmBreaks.setStatusTip(
+ self.tr("Removes all line breaks within paragraphs in the selected text")
+ )
+ self.aFmtRmBreaks.triggered.connect(lambda: self._docAction(nwDocAction.RM_BREAKS))
+ self.fmtMenu.addAction(self.aFmtRmBreaks)
+
+ return
+
+ def _buildSearchMenu(self):
+ """Assemble the Search menu.
+ """
+ # Search
+ self.srcMenu = self.addMenu(self.tr("&Search"))
+
+ # Search > Find
+ self.aFind = QAction(self.tr("Find"), self)
+ self.aFind.setStatusTip(self.tr("Find text in document"))
+ self.aFind.setShortcut("Ctrl+F")
+ self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch())
+ self.srcMenu.addAction(self.aFind)
+
+ # Search > Replace
+ self.aReplace = QAction(self.tr("Replace"), self)
+ self.aReplace.setStatusTip(self.tr("Replace text in document"))
+ if self.mainConf.osDarwin:
+ self.aReplace.setShortcut("Ctrl+=")
+ else:
+ self.aReplace.setShortcut("Ctrl+H")
+ self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace())
+ self.srcMenu.addAction(self.aReplace)
+
+ # Search > Find Next
+ self.aFindNext = QAction(self.tr("Find Next"), self)
+ self.aFindNext.setStatusTip(self.tr("Find next occurrence of text in document"))
+ if self.mainConf.osDarwin:
+ self.aFindNext.setShortcuts(["Ctrl+G", "F3"])
+ else:
+ self.aFindNext.setShortcuts(["F3", "Ctrl+G"])
+ self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext())
+ self.srcMenu.addAction(self.aFindNext)
+
+ # Search > Find Prev
+ self.aFindPrev = QAction(self.tr("Find Previous"), self)
+ self.aFindPrev.setStatusTip(self.tr("Find previous occurrence of text in document"))
+ if self.mainConf.osDarwin:
+ self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"])
+ else:
+ self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"])
+ self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True))
+ self.srcMenu.addAction(self.aFindPrev)
+
+ # Search > Replace Next
+ self.aReplaceNext = QAction(self.tr("Replace Next"), self)
+ self.aReplaceNext.setStatusTip(
+ self.tr("Find and replace next occurrence of text in document")
+ )
+ self.aReplaceNext.setShortcut("Ctrl+Shift+1")
+ self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext())
+ self.srcMenu.addAction(self.aReplaceNext)
+
return
def _buildToolsMenu(self):
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index cec8ee4e..32d5f0dc 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -106,7 +106,7 @@ class GuiTheme:
self.colKey = [0, 0, 0]
self.colVal = [0, 0, 0]
self.colSpell = [0, 0, 0]
- self.colTagErr = [0, 0, 0]
+ self.colError = [0, 0, 0]
self.colRepTag = [0, 0, 0]
self.colMod = [0, 0, 0]
@@ -373,7 +373,7 @@ class GuiTheme:
self.colKey = self._loadColour(confParser, cnfSec, "keyword")
self.colVal = self._loadColour(confParser, cnfSec, "value")
self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline")
- self.colTagErr = self._loadColour(confParser, cnfSec, "tagerror")
+ self.colError = self._loadColour(confParser, cnfSec, "errorline")
self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag")
self.colMod = self._loadColour(confParser, cnfSec, "modifier")
diff --git a/nw/guimain.py b/nw/guimain.py
index e0f5c42b..2db0add2 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -1318,7 +1318,6 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aInsEllipsis)
self.addAction(self.mainMenu.aInsPrime)
self.addAction(self.mainMenu.aInsDPrime)
- self.addAction(self.mainMenu.aInsHardBreak)
self.addAction(self.mainMenu.aInsNBSpace)
self.addAction(self.mainMenu.aInsThinSpace)
self.addAction(self.mainMenu.aInsThinNBSpace)
diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf
index cfbf4e78..1f8ba2d7 100644
--- a/tests/reference/baseConfig_novelwriter.conf
+++ b/tests/reference/baseConfig_novelwriter.conf
@@ -55,6 +55,7 @@ spelltool = internal
spellcheck = en
showtabsnspaces = False
showlineendings = False
+showmultispaces = True
bigdoclimit = 800
showfullpath = True
highlightquotes = True
diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf
index 80e77d34..1be38593 100644
--- a/tests/reference/guiPreferences_novelwriter.conf
+++ b/tests/reference/guiPreferences_novelwriter.conf
@@ -55,6 +55,7 @@ spelltool = internal
spellcheck = en
showtabsnspaces = True
showlineendings = True
+showmultispaces = True
bigdoclimit = 500
showfullpath = False
highlightquotes = False
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 955963e7..4aacf491 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -93,7 +93,8 @@ def testCoreToOdt_Convert(dummyGUI):
'bold'
' and '
'italics'
- ' text text. No format'
+ ' text text.'
+ 'No format'
''
)
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 2afa6043..af28535c 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -40,7 +40,7 @@ typeDelay = 1
stepDelay = 20
@pytest.mark.gui
-def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDir):
+def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
"""Test the document editor.
"""
# Block message box
diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py
index 9b9af54d..3da0d48c 100644
--- a/tests/test_gui/test_gui_mainmenu.py
+++ b/tests/test_gui/test_gui_mainmenu.py
@@ -236,6 +236,47 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum):
"Also text with “double” quotes which are “less tricky”.\n\n"
)
+ # Remove in-paragraph line breaks
+ nwGUI.docEditor.setText((
+ "### New Text\n\n"
+ "@char: Someone\n"
+ "@location: Somewhere\n\n"
+ "% Some comment ...\n\n"
+ "Here is some text\non multiple\nlines.\n\n"
+ "With another paragraph\nhere."
+ ))
+ nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.Trigger)
+ assert nwGUI.docEditor.getText() == (
+ "### New Text\n\n"
+ "@char: Someone\n"
+ "@location: Somewhere\n\n"
+ "% Some comment ...\n\n"
+ "Here is some text on multiple lines.\n\n"
+ "With another paragraph here.\n"
+ )
+
+ nwGUI.docEditor.setText((
+ "### New Text\n\n"
+ "@char: Someone\n"
+ "@location: Somewhere\n\n"
+ "% Some comment ...\n\n"
+ "Here is some text\non multiple\nlines.\n\n"
+ "With another paragraph\nhere."
+ ))
+ theCursor = nwGUI.docEditor.textCursor()
+ theCursor.setPosition(74)
+ theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 29)
+ nwGUI.docEditor.setTextCursor(theCursor)
+ nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.Trigger)
+ assert nwGUI.docEditor.getText() == (
+ "### New Text\n\n"
+ "@char: Someone\n"
+ "@location: Somewhere\n\n"
+ "% Some comment ...\n\n"
+ "Here is some text on multiple lines.\n\n"
+ "With another paragraph\nhere."
+ )
+
# Test Invalid Document Action
assert not nwGUI.docEditor.docAction(nwDocAction.NO_ACTION)
@@ -467,10 +508,6 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert nwGUI.docEditor.getText() == nwUnicode.U_DIVIDE
nwGUI.docEditor.clear()
- nwGUI.mainMenu.aInsHardBreak.activate(QAction.Trigger)
- assert nwGUI.docEditor.getText() == " \n"
- nwGUI.docEditor.clear()
-
nwGUI.mainMenu.aInsNBSpace.activate(QAction.Trigger)
if nwGUI.mainConf.verQtValue >= 50900:
assert nwGUI.docEditor.getText() == nwUnicode.U_NBSP
diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py
index 0e6b1e34..cb9c993b 100644
--- a/tests/test_gui/test_gui_theme.py
+++ b/tests/test_gui/test_gui_theme.py
@@ -109,7 +109,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
assert nwGUI.theTheme.colKey == [242, 119, 122]
assert nwGUI.theTheme.colVal == [204, 153, 204]
assert nwGUI.theTheme.colSpell == [242, 119, 122]
- assert nwGUI.theTheme.colTagErr == [153, 204, 153]
+ assert nwGUI.theTheme.colError == [153, 204, 153]
assert nwGUI.theTheme.colRepTag == [102, 204, 204]
assert nwGUI.theTheme.colMod == [249, 145, 57]