Add sub/superscript and partial word emphasis (#1540)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-06 11:54:49 +01:00
committed by GitHub
33 changed files with 1345 additions and 997 deletions
+5
View File
@@ -159,6 +159,7 @@ A syntax theme ``.conf`` file consists of the follwing settings:
doublequotes = 0, 0, 0 doublequotes = 0, 0, 0
singlequotes = 0, 0, 0 singlequotes = 0, 0, 0
hidden = 0, 0, 0 hidden = 0, 0, 0
shortcode = 0, 0, 0
keyword = 0, 0, 0 keyword = 0, 0, 0
value = 0, 0, 0 value = 0, 0, 0
spellcheckline = 0, 0, 0 spellcheckline = 0, 0, 0
@@ -169,3 +170,7 @@ A syntax theme ``.conf`` file consists of the follwing settings:
In the Main section, you must define at least the ``name`` setting. The Syntax colour values are In the Main section, you must define at least the ``name`` setting. The Syntax colour values are
RGB numbers of the format ``r, g, b`` where each is an integer from to 255. Omitted values default RGB numbers of the format ``r, g, b`` where each is an integer from to 255. Omitted values default
to black, except ``background`` which defaults to white, to black, except ``background`` which defaults to white,
.. versionadded:: 2.2
The `shortcode` syntax colour entry was added, so you need to update your custom themes if you
made any before version 2.2.
@@ -17,6 +17,7 @@ straightquotes = 200, 46, 0
doublequotes = 184, 200, 0 doublequotes = 184, 200, 0
singlequotes = 136, 200, 0 singlequotes = 136, 200, 0
hidden = 150, 150, 150 hidden = 150, 150, 150
shortcode = 0, 155, 200
keyword = 200, 46, 0 keyword = 200, 46, 0
value = 184, 200, 0 value = 184, 200, 0
spellcheckline = 200, 46, 0 spellcheckline = 200, 46, 0
@@ -17,6 +17,7 @@ straightquotes = 200, 0, 0
doublequotes = 0, 0, 200 doublequotes = 0, 0, 200
singlequotes = 0, 0, 200 singlequotes = 0, 0, 200
hidden = 100, 100, 100 hidden = 100, 100, 100
shortcode = 0, 100, 0
keyword = 200, 50, 50 keyword = 200, 50, 50
value = 50, 150, 50 value = 50, 150, 50
spellcheckline = 200, 0, 0 spellcheckline = 200, 0, 0
+1
View File
@@ -17,6 +17,7 @@ straightquotes = 200, 200, 200
doublequotes = 200, 200, 200 doublequotes = 200, 200, 200
singlequotes = 200, 200, 200 singlequotes = 200, 200, 200
hidden = 150, 150, 150 hidden = 150, 150, 150
shortcode = 225, 225, 225
keyword = 225, 225, 225 keyword = 225, 225, 225
value = 200, 200, 200 value = 200, 200, 200
spellcheckline = 200, 46, 0 spellcheckline = 200, 46, 0
@@ -17,6 +17,7 @@ straightquotes = 20, 20, 20
doublequotes = 20, 20, 20 doublequotes = 20, 20, 20
singlequotes = 20, 20, 20 singlequotes = 20, 20, 20
hidden = 100, 100, 100 hidden = 100, 100, 100
shortcode = 0, 0, 0
keyword = 0, 0, 0 keyword = 0, 0, 0
value = 20, 20, 20 value = 20, 20, 20
spellcheckline = 200, 0, 0 spellcheckline = 200, 0, 0
+1
View File
@@ -37,6 +37,7 @@ straightquotes = 222, 61, 58
doublequotes = 8, 145, 106 doublequotes = 8, 145, 106
singlequotes = 255, 235, 149 singlequotes = 255, 235, 149
hidden = 152, 159, 177 hidden = 152, 159, 177
shortcode = 40, 142, 215
keyword = 222, 61, 58 keyword = 222, 61, 58
value = 150, 74, 193 value = 150, 74, 193
spellcheckline = 222, 61, 58 spellcheckline = 222, 61, 58
+1
View File
@@ -37,6 +37,7 @@ straightquotes = 247, 140, 108
doublequotes = 173, 219, 103 doublequotes = 173, 219, 103
singlequotes = 255, 235, 149 singlequotes = 255, 235, 149
hidden = 99, 119, 119 hidden = 99, 119, 119
shortcode = 130, 170, 255
keyword = 247, 140, 108 keyword = 247, 140, 108
value = 199, 146, 234 value = 199, 146, 234
spellcheckline = 247, 140, 108 spellcheckline = 247, 140, 108
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
doublequotes = 42, 161, 152 doublequotes = 42, 161, 152
singlequotes = 42, 161, 152 singlequotes = 42, 161, 152
hidden = 147, 161, 161 hidden = 147, 161, 161
shortcode = 147, 161, 161
keyword = 133, 153, 0 keyword = 133, 153, 0
value = 203, 75, 22 value = 203, 75, 22
spellcheckline = 203, 75, 22 spellcheckline = 203, 75, 22
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
doublequotes = 42, 161, 152 doublequotes = 42, 161, 152
singlequotes = 42, 161, 152 singlequotes = 42, 161, 152
hidden = 88, 110, 117 hidden = 88, 110, 117
shortcode = 88, 110, 117
keyword = 133, 153, 0 keyword = 133, 153, 0
value = 203, 75, 22 value = 203, 75, 22
spellcheckline = 203, 75, 22 spellcheckline = 203, 75, 22
+1
View File
@@ -37,6 +37,7 @@ straightquotes = 240, 40, 41
doublequotes = 113, 140, 0 doublequotes = 113, 140, 0
singlequotes = 234, 183, 0 singlequotes = 234, 183, 0
hidden = 142, 144, 140 hidden = 142, 144, 140
shortcode = 66, 113, 174
keyword = 240, 40, 41 keyword = 240, 40, 41
value = 137, 89, 168 value = 137, 89, 168
spellcheckline = 240, 40, 41 spellcheckline = 240, 40, 41
@@ -37,6 +37,7 @@ straightquotes = 204, 102, 102
doublequotes = 181, 189, 104 doublequotes = 181, 189, 104
singlequotes = 240, 198, 116 singlequotes = 240, 198, 116
hidden = 150, 152, 150 hidden = 150, 152, 150
shortcode = 129, 162, 190
keyword = 204, 102, 102 keyword = 204, 102, 102
value = 178, 148, 187 value = 178, 148, 187
spellcheckline = 204, 102, 102 spellcheckline = 204, 102, 102
@@ -37,6 +37,7 @@ straightquotes = 255, 157, 164
doublequotes = 209, 241, 169 doublequotes = 209, 241, 169
singlequotes = 255, 238, 173 singlequotes = 255, 238, 173
hidden = 114, 133, 183 hidden = 114, 133, 183
shortcode = 187, 218, 255
keyword = 255, 157, 164 keyword = 255, 157, 164
value = 235, 187, 255 value = 235, 187, 255
spellcheckline = 255, 157, 164 spellcheckline = 255, 157, 164
@@ -37,6 +37,7 @@ straightquotes = 213, 78, 83
doublequotes = 185, 202, 74 doublequotes = 185, 202, 74
singlequotes = 231, 197, 71 singlequotes = 231, 197, 71
hidden = 150, 152, 150 hidden = 150, 152, 150
shortcode = 122, 166, 218
keyword = 213, 78, 83 keyword = 213, 78, 83
value = 195, 151, 216 value = 195, 151, 216
spellcheckline = 213, 78, 83 spellcheckline = 213, 78, 83
@@ -37,6 +37,7 @@ straightquotes = 242, 119, 122
doublequotes = 153, 204, 153 doublequotes = 153, 204, 153
singlequotes = 255, 204, 102 singlequotes = 255, 204, 102
hidden = 153, 153, 153 hidden = 153, 153, 153
shortcode = 102, 153, 204
keyword = 242, 119, 122 keyword = 242, 119, 122
value = 204, 153, 204 value = 204, 153, 204
spellcheckline = 242, 119, 122 spellcheckline = 242, 119, 122
+7
View File
@@ -23,6 +23,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import re
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
@@ -59,6 +61,11 @@ class nwRegEx:
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|sup|sub)\])"
FMT_SV = r"(?<!\\)(\[(?i)(?:fn|footnote):)(.+?)(?<!\\)(\])"
# Pre-Compiled RegEx
RX_SC = re.compile(FMT_SC)
# END Class nwRegEx # END Class nwRegEx
+1 -1
View File
@@ -184,7 +184,7 @@ class NWBuildDocument:
filtered = self._setupBuild(makeObj) filtered = self._setupBuild(makeObj)
if extendedMd: if extendedMd:
makeObj.setGitHubMarkdown() makeObj.setExtendedMarkdown()
else: else:
makeObj.setStandardMarkdown() makeObj.setStandardMarkdown()
+8 -3
View File
@@ -38,7 +38,7 @@ from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
@@ -1229,6 +1229,10 @@ def countWords(text: str) -> tuple[int, int, int]:
if nwUnicode.U_EMDASH in text: if nwUnicode.U_EMDASH in text:
text = text.replace(nwUnicode.U_EMDASH, " ") text = text.replace(nwUnicode.U_EMDASH, " ")
# Strip shortcodes
if "[" in text:
text = nwRegEx.RX_SC.sub("", text)
for line in text.splitlines(): for line in text.splitlines():
countPara = True countPara = True
@@ -1241,9 +1245,10 @@ def countWords(text: str) -> tuple[int, int, int]:
continue continue
if line[0] == "[": if line[0] == "[":
if line.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")): check = line.lower()
if check.startswith(("[newpage]", "[new page]", "[vspace]")):
continue continue
elif line.startswith("[VSPACE:") and line.endswith("]"): elif check.startswith("[vspace:") and line.endswith("]"):
continue continue
elif line[0] == "#": elif line[0] == "#":
+23 -23
View File
@@ -31,7 +31,7 @@ from pathlib import Path
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import formatTimeStamp from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode, trConst
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer, stripEscape from novelwriter.core.tokenizer import Tokenizer, stripEscape
@@ -131,6 +131,8 @@ class ToHtml(Tokenizer):
self.FMT_I_E: "</i>", self.FMT_I_E: "</i>",
self.FMT_D_B: "<span style='text-decoration: line-through;'>", self.FMT_D_B: "<span style='text-decoration: line-through;'>",
self.FMT_D_E: "</span>", self.FMT_D_E: "</span>",
self.FMT_U_B: "<u>",
self.FMT_U_E: "</u>",
} }
else: else:
htmlTags = { # HTML5 (for export) htmlTags = { # HTML5 (for export)
@@ -140,8 +142,15 @@ class ToHtml(Tokenizer):
self.FMT_I_E: "</em>", self.FMT_I_E: "</em>",
self.FMT_D_B: "<del>", self.FMT_D_B: "<del>",
self.FMT_D_E: "</del>", self.FMT_D_E: "</del>",
self.FMT_U_B: "<span style='text-decoration: underline;'>",
self.FMT_U_E: "</span>",
} }
htmlTags[self.FMT_SUP_B] = "<sup>"
htmlTags[self.FMT_SUP_E] = "</sup>"
htmlTags[self.FMT_SUB_B] = "<sub>"
htmlTags[self.FMT_SUB_E] = "</sub>"
if self._isNovel and self._genMode != self.M_PREVIEW: if self._isNovel and self._genMode != self.M_PREVIEW:
# For story files, we bump the titles one level up # For story files, we bump the titles one level up
h1Cl = " class='title'" h1Cl = " class='title'"
@@ -172,18 +181,16 @@ class ToHtml(Tokenizer):
for c in tText: for c in tText:
if c == "<": if c == "<":
cText.append("&lt;") cText.append("&lt;")
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat] tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
i += 4 i += 4
elif c == ">": elif c == ">":
cText.append("&gt;") cText.append("&gt;")
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat] tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
i += 4 i += 4
else: else:
cText.append(c) cText.append(c)
i += 1 i += 1
tText = "".join(cText) tText = "".join(cText)
else: else:
# If we don't have formatting, we can do a plain replace # If we don't have formatting, we can do a plain replace
tText = tText.replace("<", "&lt;").replace(">", "&gt;") tText = tText.replace("<", "&lt;").replace(">", "&gt;")
@@ -275,8 +282,8 @@ class ToHtml(Tokenizer):
tTemp = tText tTemp = tText
if pStyle is None: if pStyle is None:
pStyle = hStyle pStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat): for pos, fmt in reversed(tFormat):
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:] tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
para.append(stripEscape(tTemp.rstrip())) para.append(stripEscape(tTemp.rstrip()))
elif tType == self.T_SYNOPSIS and self._doSynopsis: elif tType == self.T_SYNOPSIS and self._doSynopsis:
@@ -467,25 +474,18 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, text: str) -> str: def _formatKeywords(self, text: str) -> str:
"""Apply HTML formatting to keywords.""" """Apply HTML formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text) valid, bits, _ = self._project.index.scanThis("@"+text)
if not valid or not bits: if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
return "" return ""
result = "" result = f"<span class='tags'>{trConst(nwLabels.KEY_NAME[bits[0]])}:</span> "
tags = [] if len(bits) > 1:
if bits[0] in nwLabels.KEY_NAME: if bits[0] == nwKeyWords.TAG_KEY:
result += f"<span class='tags'>{nwLabels.KEY_NAME[bits[0]]}:</span> " result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
if len(bits) > 1: else:
if bits[0] == nwKeyWords.TAG_KEY: if self._genMode == self.M_PREVIEW:
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>" result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
else: else:
if self._genMode == self.M_PREVIEW: result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
for tTag in bits[1:]:
tags.append(f"<a href='#{bits[0][1:]}={tTag}'>{tTag}</a>")
result += ", ".join(tags)
else:
for tTag in bits[1:]:
tags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
result += ", ".join(tags)
return result return result
+85 -41
View File
@@ -31,7 +31,6 @@ import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from time import time from time import time
from pathlib import Path from pathlib import Path
from operator import itemgetter
from functools import partial from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression from PyQt5.QtCore import QCoreApplication, QRegularExpression
@@ -43,15 +42,14 @@ from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
def stripEscape(text) -> str: def stripEscape(text) -> str:
"""Helper function to strip escaped Markdown characters from """Strip escaped Markdown characters from paragraph text."""
paragraph text.
"""
if "\\" in text: if "\\" in text:
# Checking first is slightly slower when there are escaped return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text)
# characters in the text, but significantly faster when not
return text.replace(r"\*", "*").replace(r"\~", "~").replace(r"\_", "_")
return text return text
@@ -65,12 +63,18 @@ class Tokenizer(ABC):
""" """
# In-Text Format # In-Text Format
FMT_B_B = 1 # Begin bold FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics FMT_I_E = 4 # End italics
FMT_D_B = 5 # Begin strikeout FMT_D_B = 5 # Begin strikeout
FMT_D_E = 6 # End strikeout FMT_D_E = 6 # End strikeout
FMT_U_B = 7 # Begin underline
FMT_U_E = 8 # End underline
FMT_SUP_B = 9 # Begin superscript
FMT_SUP_E = 10 # End superscript
FMT_SUB_B = 11 # Begin subscript
FMT_SUB_E = 12 # End subscript
# Block Type # Block Type
T_EMPTY = 1 # Empty line (new paragraph) T_EMPTY = 1 # Empty line (new paragraph)
@@ -166,6 +170,24 @@ class Tokenizer(ABC):
# Cached Translations # Cached Translations
self._trSynopsis = self.tr("Synopsis") self._trSynopsis = self.tr("Synopsis")
# Format RegEx
self._rxMarkdown = [
(QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]),
(QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]),
(QRegularExpression(nwRegEx.FMT_ST), [0, self.FMT_D_B, 0, self.FMT_D_E]),
]
self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC)
self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV)
self._shortCodeFmt = {
"[i]": self.FMT_I_B, "[/i]": self.FMT_I_E,
"[b]": self.FMT_B_B, "[/b]": self.FMT_B_E,
"[s]": self.FMT_D_B, "[/s]": self.FMT_D_E,
"[u]": self.FMT_U_B, "[/u]": self.FMT_U_E,
"[sup]": self.FMT_SUP_B, "[/sup]": self.FMT_SUP_E,
"[sub]": self.FMT_SUB_B, "[/sub]": self.FMT_SUB_E,
}
return return
## ##
@@ -387,19 +409,12 @@ class Tokenizer(ABC):
4: The internal formatting map of the text, self.FMT_* 4: The internal formatting map of the text, self.FMT_*
5: The style of the block, self.A_* 5: The style of the block, self.A_*
""" """
# RegExes for adding formatting tags within text lines
rxFormats = [
(QRegularExpression(nwRegEx.FMT_EI), [None, self.FMT_I_B, None, self.FMT_I_E]),
(QRegularExpression(nwRegEx.FMT_EB), [None, self.FMT_B_B, None, self.FMT_B_E]),
(QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
]
self._tokens = [] self._tokens = []
tmpMarkdown = [] tmpMarkdown = []
nHead = 0 nHead = 0
breakNext = False breakNext = False
for aLine in self._text.splitlines(): for aLine in self._text.splitlines():
sLine = aLine.strip() sLine = aLine.strip().lower()
# Check for blank lines # Check for blank lines
if len(sLine) == 0: if len(sLine) == 0:
@@ -422,18 +437,21 @@ class Tokenizer(ABC):
if aLine[0] == "[": if aLine[0] == "[":
# Parse special formatting line # Parse special formatting line
# This must be a separate if statement, as it may not
# reach a continue statement and must thefore proceed to
# check other formats.
if sLine in ("[NEWPAGE]", "[NEW PAGE]"): if sLine in ("[newpage]", "[new page]"):
breakNext = True breakNext = True
continue continue
elif sLine == "[VSPACE]": elif sLine == "[vspace]":
self._tokens.append( self._tokens.append(
(self.T_SKIP, nHead, "", None, sAlign) (self.T_SKIP, nHead, "", None, sAlign)
) )
continue continue
elif sLine.startswith("[VSPACE:") and sLine.endswith("]"): elif sLine.startswith("[vspace:") and sLine.endswith("]"):
nSkip = checkInt(sLine[8:-1], 0) nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1: if nSkip >= 1:
self._tokens.append( self._tokens.append(
@@ -445,7 +463,7 @@ class Tokenizer(ABC):
] ]
continue continue
elif aLine[0] == "%": if aLine[0] == "%":
cLine = aLine[1:].lstrip() cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower() synTag = cLine[:9].lower()
if synTag == "synopsis:": if synTag == "synopsis:":
@@ -570,23 +588,10 @@ class Tokenizer(ABC):
if indRight: if indRight:
sAlign |= self.A_IND_R sAlign |= self.A_IND_R
# Otherwise we use RegEx to find formatting tags within a line of text # Process formats
fmtPos = [] tLine, fmtPos = self._extractFormats(aLine)
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._tokens.append(( self._tokens.append((
self.T_TEXT, nHead, aLine, fmtPos, sAlign self.T_TEXT, nHead, tLine, fmtPos, sAlign
)) ))
if self._keepMarkdown: if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
@@ -763,6 +768,45 @@ class Tokenizer(ABC):
json.dump(data, fObj, indent=2) json.dump(data, fObj, indent=2)
return return
##
# Internal Functions
##
def _extractFormats(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Extract format markers from a text paragraph."""
temp = []
# Match Markdown
for regEx, fmts in self._rxMarkdown:
rxItt = regEx.globalMatch(text, 0)
while rxItt.hasNext():
rxMatch = rxItt.next()
temp.extend(
[rxMatch.capturedStart(n), rxMatch.capturedLength(n), fmt]
for n, fmt in enumerate(fmts) if fmt > 0
)
# Match Shortcodes
rxItt = self._rxShortCodes.globalMatch(text, 0)
while rxItt.hasNext():
rxMatch = rxItt.next()
temp.append([
rxMatch.capturedStart(1),
rxMatch.capturedLength(1),
self._shortCodeFmt.get(rxMatch.captured(1).lower(), 0)
])
# Post-process text and format markers
result = text
formats = []
for pos, n, fmt in reversed(sorted(temp, key=lambda x: x[0])):
if fmt > 0:
result = result[:pos] + result[pos+n:]
formats = [(p-n, f) for p, f in formats]
formats.insert(0, (pos, fmt))
return result, formats
# END Class Tokenizer # END Class Tokenizer
+24 -12
View File
@@ -27,7 +27,7 @@ import logging
from pathlib import Path from pathlib import Path
from novelwriter.constants import nwHeadFmt, nwLabels from novelwriter.constants import nwHeadFmt, nwLabels, trConst
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer from novelwriter.core.tokenizer import Tokenizer
@@ -38,12 +38,12 @@ class ToMarkdown(Tokenizer):
"""Core: Markdown Document Writer """Core: Markdown Document Writer
Extend the Tokenizer class to writer Markdown output. It supports Extend the Tokenizer class to writer Markdown output. It supports
both Standard Markdown and GitHub Flavour Markdown (Extended). The both Standard Markdown and Extended Markdown. The class also
class also supports concatenating novelWriter markup files. supports concatenating novelWriter markup files.
""" """
M_STD = 0 # Standard Markdown M_STD = 0 # Standard Markdown
M_GH = 1 # GitHub Markdown M_EXT = 1 # Extended Markdown
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
super().__init__(project) super().__init__(project)
@@ -69,9 +69,9 @@ class ToMarkdown(Tokenizer):
self._genMode = self.M_STD self._genMode = self.M_STD
return return
def setGitHubMarkdown(self) -> None: def setExtendedMarkdown(self) -> None:
"""Set the converter to use GitHub Markdown formatting.""" """Set the converter to use Extended Markdown formatting."""
self._genMode = self.M_GH self._genMode = self.M_EXT
return return
## ##
@@ -85,7 +85,7 @@ class ToMarkdown(Tokenizer):
def doConvert(self) -> None: def doConvert(self) -> None:
"""Convert the list of text tokens into a Markdown document.""" """Convert the list of text tokens into a Markdown document."""
if self._genMode == self.M_STD: if self._genMode == self.M_STD:
# Standard # Standard Markdown
mdTags = { mdTags = {
self.FMT_B_B: "**", self.FMT_B_B: "**",
self.FMT_B_E: "**", self.FMT_B_E: "**",
@@ -93,9 +93,15 @@ class ToMarkdown(Tokenizer):
self.FMT_I_E: "_", self.FMT_I_E: "_",
self.FMT_D_B: "", self.FMT_D_B: "",
self.FMT_D_E: "", self.FMT_D_E: "",
self.FMT_U_B: "",
self.FMT_U_E: "",
self.FMT_SUP_B: "",
self.FMT_SUP_E: "",
self.FMT_SUB_B: "",
self.FMT_SUB_E: "",
} }
else: else:
# GitHub # Extended Markdown
mdTags = { mdTags = {
self.FMT_B_B: "**", self.FMT_B_B: "**",
self.FMT_B_E: "**", self.FMT_B_E: "**",
@@ -103,6 +109,12 @@ class ToMarkdown(Tokenizer):
self.FMT_I_E: "_", self.FMT_I_E: "_",
self.FMT_D_B: "~~", self.FMT_D_B: "~~",
self.FMT_D_E: "~~", self.FMT_D_E: "~~",
self.FMT_U_B: "",
self.FMT_U_E: "",
self.FMT_SUP_B: "^",
self.FMT_SUP_E: "^",
self.FMT_SUB_B: "~",
self.FMT_SUB_E: "~",
} }
self._result = "" self._result = ""
@@ -150,8 +162,8 @@ class ToMarkdown(Tokenizer):
elif tType == self.T_TEXT: elif tType == self.T_TEXT:
tTemp = tText tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat): for pos, fmt in reversed(tFormat):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:] tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}"
para.append(tTemp.rstrip()) para.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self._doSynopsis: elif tType == self.T_SYNOPSIS and self._doSynopsis:
@@ -197,7 +209,7 @@ class ToMarkdown(Tokenizer):
result = "" result = ""
if bits[0] in nwLabels.KEY_NAME: if bits[0] in nwLabels.KEY_NAME:
result += f"**{nwLabels.KEY_NAME[bits[0]]}:** " result += f"**{trConst(nwLabels.KEY_NAME[bits[0]])}:** "
if len(bits) > 1: if len(bits) > 1:
result += ", ".join(bits[1:]) result += ", ".join(bits[1:])
+126 -96
View File
@@ -29,6 +29,7 @@ from __future__ import annotations
import logging import logging
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Sequence
from hashlib import sha256 from hashlib import sha256
from pathlib import Path from pathlib import Path
from zipfile import ZipFile from zipfile import ZipFile
@@ -36,7 +37,7 @@ from datetime import datetime
from novelwriter import __version__ from novelwriter import __version__
from novelwriter.common import xmlIndent from novelwriter.common import xmlIndent
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, trConst
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer, stripEscape from novelwriter.core.tokenizer import Tokenizer, stripEscape
@@ -82,11 +83,17 @@ TAG_STNM = _mkTag("text", "style-name")
X_BLD = 0x01 # Bold format X_BLD = 0x01 # Bold format
X_ITA = 0x02 # Italic format X_ITA = 0x02 # Italic format
X_DEL = 0x04 # Strikethrough format X_DEL = 0x04 # Strikethrough format
X_UND = 0x08 # Underline format
X_SUP = 0x10 # Superscript
X_SUB = 0x20 # Subscript
# Formatting Masks # Formatting Masks
M_BLD = ~X_BLD M_BLD = ~X_BLD
M_ITA = ~X_ITA M_ITA = ~X_ITA
M_DEL = ~X_DEL M_DEL = ~X_DEL
M_UND = ~X_UND
M_SUP = ~X_SUP
M_SUB = ~X_SUB
class ToOdt(Tokenizer): class ToOdt(Tokenizer):
@@ -385,17 +392,8 @@ class ToOdt(Tokenizer):
"""Convert the list of text tokens into XML elements.""" """Convert the list of text tokens into XML elements."""
self._result = "" # Not used, but cleared just in case self._result = "" # Not used, but cleared just in case
odtTags = { pFmt = []
self.FMT_B_B: "_B", # Bold open format pText = []
self.FMT_B_E: "b_", # Bold close format
self.FMT_I_B: "I", # Italic open format
self.FMT_I_E: "i", # Italic close format
self.FMT_D_B: "_S", # Strikethrough open format
self.FMT_D_E: "s_", # Strikethrough close format
}
fmt = []
para = []
pStyle = None pStyle = None
for tType, _, tText, tFormat, tStyle in self._tokens: for tType, _, tText, tFormat, tStyle in self._tokens:
@@ -429,19 +427,21 @@ class ToOdt(Tokenizer):
# Process Text Types # Process Text Types
if tType == self.T_EMPTY: if tType == self.T_EMPTY:
if len(para) > 1 and pStyle is not None: if len(pText) > 1 and pStyle is not None:
if self._doJustify: if self._doJustify:
pStyle.setTextAlign("left") pStyle.setTextAlign("left")
if len(para) > 0 and pStyle is not None: if len(pText) > 0 and pStyle is not None:
tTemp = "\n".join(para) tTxt = ""
fTemp = " ".join(fmt) tFmt = []
tTxt = tTemp.rstrip() for nText, nFmt in zip(pText, pFmt):
tFmt = fTemp[:len(tTxt)] tLen = len(tTxt)
self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt) tTxt += f"{nText}\n"
tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
self._addTextPar("Text_20_body", pStyle, tTxt.rstrip(), tFmt=tFmt)
fmt = [] pFmt = []
para = [] pText = []
pStyle = None pStyle = None
elif tType == self.T_TITLE: elif tType == self.T_TITLE:
@@ -477,15 +477,8 @@ class ToOdt(Tokenizer):
elif tType == self.T_TEXT: elif tType == self.T_TEXT:
if pStyle is None: if pStyle is None:
pStyle = oStyle pStyle = oStyle
pText.append(tText)
tFmt = " "*len(tText) pFmt.append(tFormat)
for xPos, xLen, xFmt in tFormat:
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
tTxt = tText.rstrip()
tFmt = tFmt[:len(tTxt)]
para.append(tTxt)
fmt.append(tFmt)
elif tType == self.T_SYNOPSIS and self._doSynopsis: elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText) tTemp, fTemp = self._formatSynopsis(tText)
@@ -559,46 +552,39 @@ class ToOdt(Tokenizer):
# Internal Functions # Internal Functions
## ##
def _formatSynopsis(self, text: str) -> tuple[str, str]: def _formatSynopsis(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to synopsis lines.""" """Apply formatting to synopsis lines."""
sSynop = self._localLookup("Synopsis") name = self._localLookup("Synopsis")
rTxt = "**{0}:** {1}".format(sSynop, text) rTxt = f"{name}: {text}"
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(text)) rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt return rTxt, rFmt
def _formatComments(self, text: str) -> tuple[str, str]: def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to comments.""" """Apply formatting to comments."""
sComm = self._localLookup("Comment") name = self._localLookup("Comment")
rTxt = "**{0}:** {1}".format(sComm, text) rTxt = f"{name}: {text}"
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(text)) rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt return rTxt, rFmt
def _formatKeywords(self, text: str) -> tuple[str, str]: def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to keywords.""" """Apply formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text) valid, bits, _ = self._project.index.scanThis("@"+text)
if not valid or not bits: if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
return "", "" return "", []
rTxt = "" rTxt = f"{trConst(nwLabels.KEY_NAME[bits[0]])}: "
rFmt = "" rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
if bits[0] in nwLabels.KEY_NAME: if len(bits) > 1:
text = nwLabels.KEY_NAME[bits[0]] if bits[0] == nwKeyWords.TAG_KEY:
rTxt += "**{0}:** ".format(text) rTxt += bits[1]
rFmt += "_B{0} b_ ".format(" "*len(text)) else:
if len(bits) > 1: rTxt += ", ".join(bits[1:])
if bits[0] == nwKeyWords.TAG_KEY:
rTxt += bits[1]
rFmt += " "*len(bits[1])
else:
tTags = ", ".join(bits[1:])
rTxt += tTags
rFmt += (" "*len(tTags))
return rTxt, rFmt return rTxt, rFmt
def _addTextPar( def _addTextPar(
self, styleName: str, oStyle: ODTParagraphStyle, tText: str, tFmt: str = "", self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
isHead: bool = False, oLevel: str | None = None tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
) -> None: ) -> None:
"""Add a text paragraph to the text XML element.""" """Add a text paragraph to the text XML element."""
tAttr = {} tAttr = {}
@@ -616,58 +602,59 @@ class ToOdt(Tokenizer):
if not tText: if not tText:
return return
## # Loop Over Fragments
# Process Formatting # ===================
##
if len(tText) != len(tFmt):
# Generate an empty format if there isn't any or it doesn't match
tFmt = " "*len(tText)
# The formatting loop
tTemp = ""
xFmt = 0x00
pFmt = 0x00
pErr = 0
parProc = XMLParagraph(xElem) parProc = XMLParagraph(xElem)
for i, c in enumerate(tText): pErr = 0
xFmt = 0x00
tFrag = ""
fLast = 0
for fPos, fFmt in tFmt:
if tFmt[i] == " ": # Add the text up to the current fragment
tTemp += c if tFrag := tText[fLast:fPos]:
elif tFmt[i] == "_": if xFmt == 0x00:
continue parProc.appendText(tFrag)
elif tFmt[i] == "B": else:
parProc.appendSpan(tFrag, self._textStyle(xFmt))
# Calculate the change of format
if fFmt == self.FMT_B_B:
xFmt |= X_BLD xFmt |= X_BLD
elif tFmt[i] == "b": elif fFmt == self.FMT_B_E:
xFmt &= M_BLD xFmt &= M_BLD
elif tFmt[i] == "I": elif fFmt == self.FMT_I_B:
xFmt |= X_ITA xFmt |= X_ITA
elif tFmt[i] == "i": elif fFmt == self.FMT_I_E:
xFmt &= M_ITA xFmt &= M_ITA
elif tFmt[i] == "S": elif fFmt == self.FMT_D_B:
xFmt |= X_DEL xFmt |= X_DEL
elif tFmt[i] == "s": elif fFmt == self.FMT_D_E:
xFmt &= M_DEL xFmt &= M_DEL
elif fFmt == self.FMT_U_B:
xFmt |= X_UND
elif fFmt == self.FMT_U_E:
xFmt &= M_UND
elif fFmt == self.FMT_SUP_B:
xFmt |= X_SUP
elif fFmt == self.FMT_SUP_E:
xFmt &= M_SUP
elif fFmt == self.FMT_SUB_B:
xFmt |= X_SUB
elif fFmt == self.FMT_SUB_E:
xFmt &= M_SUB
else: else:
pErr += 1 pErr += 1
if xFmt != pFmt: fLast = fPos
if pFmt == 0x00:
parProc.appendText(tTemp)
tTemp = ""
else:
parProc.appendSpan(tTemp, self._textStyle(pFmt))
tTemp = ""
pFmt = xFmt if tFrag := tText[fLast:]:
if xFmt == 0x00:
# Save what remains in the buffer parProc.appendText(tFrag)
if pFmt == 0x00: else:
parProc.appendText(tTemp) parProc.appendSpan(tFrag, self._textStyle(xFmt))
else:
parProc.appendSpan(tTemp, self._textStyle(pFmt))
if pErr > 0: if pErr > 0:
self._errData.append("Unknown format tag encountered") self._errData.append("Unknown format tag encountered")
@@ -713,6 +700,14 @@ class ToOdt(Tokenizer):
if hFmt & X_DEL: if hFmt & X_DEL:
newStyle.setStrikeStyle("solid") newStyle.setStrikeStyle("solid")
newStyle.setStrikeType("single") newStyle.setStrikeType("single")
if hFmt & X_UND:
newStyle.setUnderlineStyle("solid")
newStyle.setUnderlineWidth("auto")
newStyle.setUnderlineColour("font-color")
if hFmt & X_SUP:
newStyle.setTextPosition("super")
if hFmt & X_SUB:
newStyle.setTextPosition("sub")
self._autoText[hFmt] = (newName, newStyle) self._autoText[hFmt] = (newName, newStyle)
@@ -1256,16 +1251,23 @@ class ODTTextStyle:
""" """
VALID_WEIGHT = ["normal", "inherit", "bold"] VALID_WEIGHT = ["normal", "inherit", "bold"]
VALID_STYLE = ["normal", "inherit", "italic"] VALID_STYLE = ["normal", "inherit", "italic"]
VALID_POS = ["super", "sub"]
VALID_LSTYLE = ["none", "solid"] VALID_LSTYLE = ["none", "solid"]
VALID_LTYPE = ["none", "single", "double"] VALID_LTYPE = ["single", "double"]
VALID_LWIDTH = ["auto"]
VALID_LCOL = ["font-color"]
def __init__(self) -> None: def __init__(self) -> None:
# Text Attributes # Text Attributes
self._tAttr = { self._tAttr = {
"font-weight": ["fo", None], "font-weight": ["fo", None],
"font-style": ["fo", None], "font-style": ["fo", None],
"text-position": ["style", None],
"text-line-through-style": ["style", None], "text-line-through-style": ["style", None],
"text-line-through-type": ["style", None], "text-line-through-type": ["style", None],
"text-underline-style": ["style", None],
"text-underline-width": ["style", None],
"text-underline-color": ["style", None],
} }
return return
@@ -1287,6 +1289,13 @@ class ODTTextStyle:
self._tAttr["font-style"][1] = None self._tAttr["font-style"][1] = None
return return
def setTextPosition(self, value: str | None) -> None:
if value in self.VALID_POS:
self._tAttr["text-position"][1] = f"{value} 58%"
else:
self._tAttr["text-position"][1] = None
return
def setStrikeStyle(self, value: str | None) -> None: def setStrikeStyle(self, value: str | None) -> None:
if value in self.VALID_LSTYLE: if value in self.VALID_LSTYLE:
self._tAttr["text-line-through-style"][1] = value self._tAttr["text-line-through-style"][1] = value
@@ -1301,6 +1310,27 @@ class ODTTextStyle:
self._tAttr["text-line-through-type"][1] = None self._tAttr["text-line-through-type"][1] = None
return return
def setUnderlineStyle(self, value: str | None) -> None:
if value in self.VALID_LSTYLE:
self._tAttr["text-underline-style"][1] = value
else:
self._tAttr["text-underline-style"][1] = None
return
def setUnderlineWidth(self, value: str | None) -> None:
if value in self.VALID_LWIDTH:
self._tAttr["text-underline-width"][1] = value
else:
self._tAttr["text-underline-width"][1] = None
return
def setUnderlineColour(self, value: str | None) -> None:
if value in self.VALID_LCOL:
self._tAttr["text-underline-color"][1] = value
else:
self._tAttr["text-underline-color"][1] = None
return
## ##
# Methods # Methods
## ##
+15 -22
View File
@@ -55,6 +55,7 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
from novelwriter.common import minmax, transferCase from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.core.index import countWords from novelwriter.core.index import countWords
from novelwriter.core.document import NWDocument
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
from novelwriter.gui.editordocument import GuiTextDocument from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.wheeleventfilter import WheelEventFilter from novelwriter.extensions.wheeleventfilter import WheelEventFilter
@@ -408,12 +409,6 @@ class GuiDocEditor(QPlainTextEdit):
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META) self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
return return
def redrawText(self) -> None:
"""Redraw the text by marking the document content as dirty."""
self._qDocument.markContentsDirty(0, self._qDocument.characterCount())
self.updateDocMargins()
return
def replaceText(self, text: str) -> None: def replaceText(self, text: str) -> None:
"""Replace the text of the current document with the provided """Replace the text of the current document with the provided
text. This also clears undo history. text. This also clears undo history.
@@ -735,19 +730,17 @@ class GuiDocEditor(QPlainTextEdit):
"""Tell the user where on the file system the file in the editor """Tell the user where on the file system the file in the editor
is saved. is saved.
""" """
if self._nwDocument is None: if isinstance(self._nwDocument, NWDocument):
logger.error("No document open") SHARED.info(
return "<br>".join([
SHARED.info( self.tr("Document Details"),
"<br>".join([ ""*40,
self.tr("Document Details"), self.tr("Created: {0}").format(self._nwDocument.createdDate),
""*40, self.tr("Updated: {0}").format(self._nwDocument.updatedDate),
self.tr("Created: {0}").format(self._nwDocument.createdDate), ]),
self.tr("Updated: {0}").format(self._nwDocument.updatedDate), details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
]), log=False
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation), )
log=False
)
return return
def insertText(self, insert: str | nwDocInsert) -> bool: def insertText(self, insert: str | nwDocInsert) -> bool:
@@ -775,15 +768,15 @@ class GuiDocEditor(QPlainTextEdit):
newBlock = True newBlock = True
goAfter = True goAfter = True
elif insert == nwDocInsert.NEW_PAGE: elif insert == nwDocInsert.NEW_PAGE:
text = "[NEW PAGE]" text = "[newpage]"
newBlock = True newBlock = True
goAfter = False goAfter = False
elif insert == nwDocInsert.VSPACE_S: elif insert == nwDocInsert.VSPACE_S:
text = "[VSPACE]" text = "[vspace]"
newBlock = True newBlock = True
goAfter = False goAfter = False
elif insert == nwDocInsert.VSPACE_M: elif insert == nwDocInsert.VSPACE_M:
text = "[VSPACE:2]" text = "[vspace:2]"
newBlock = True newBlock = True
goAfter = False goAfter = False
else: else:
+26 -8
View File
@@ -39,7 +39,7 @@ from novelwriter.constants import nwRegEx, nwUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b") SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
@@ -69,6 +69,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._colDialD = QColor(0, 0, 0) self._colDialD = QColor(0, 0, 0)
self._colDialS = QColor(0, 0, 0) self._colDialS = QColor(0, 0, 0)
self._colHidden = QColor(0, 0, 0) self._colHidden = QColor(0, 0, 0)
self._colCode = QColor(0, 0, 0)
self._colKey = QColor(0, 0, 0) self._colKey = QColor(0, 0, 0)
self._colVal = QColor(0, 0, 0) self._colVal = QColor(0, 0, 0)
self._colSpell = QColor(0, 0, 0) self._colSpell = QColor(0, 0, 0)
@@ -95,6 +96,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._colDialD = QColor(*SHARED.theme.colDialD) self._colDialD = QColor(*SHARED.theme.colDialD)
self._colDialS = QColor(*SHARED.theme.colDialS) self._colDialS = QColor(*SHARED.theme.colDialS)
self._colHidden = QColor(*SHARED.theme.colHidden) self._colHidden = QColor(*SHARED.theme.colHidden)
self._colCode = QColor(*SHARED.theme.colCode)
self._colKey = QColor(*SHARED.theme.colKey) self._colKey = QColor(*SHARED.theme.colKey)
self._colVal = QColor(*SHARED.theme.colVal) self._colVal = QColor(*SHARED.theme.colVal)
self._colSpell = QColor(*SHARED.theme.colSpell) self._colSpell = QColor(*SHARED.theme.colSpell)
@@ -127,6 +129,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"dialogue3": self._makeFormat(self._colDialS), "dialogue3": self._makeFormat(self._colDialS),
"replace": self._makeFormat(self._colRepTag), "replace": self._makeFormat(self._colRepTag),
"hidden": self._makeFormat(self._colHidden), "hidden": self._makeFormat(self._colHidden),
"code": self._makeFormat(self._colCode),
"keyword": self._makeFormat(self._colKey), "keyword": self._makeFormat(self._colKey),
"modifier": self._makeFormat(self._colMod), "modifier": self._makeFormat(self._colMod),
"value": self._makeFormat(self._colVal, "underline"), "value": self._makeFormat(self._colVal, "underline"),
@@ -205,6 +208,22 @@ class GuiDocHighlighter(QSyntaxHighlighter):
} }
)) ))
# Shortcodes
self._hRules.append((
nwRegEx.FMT_SC, {
1: self._hStyles["code"],
}
))
# Shortcodes w/Value
self._hRules.append((
nwRegEx.FMT_SV, {
1: self._hStyles["code"],
2: self._hStyles["codevalue"],
3: self._hStyles["code"],
}
))
# Alignment Tags # Alignment Tags
self._hRules.append(( self._hRules.append((
r"(^>{1,2}|<{1,2}$)", { r"(^>{1,2}|<{1,2}$)", {
@@ -347,18 +366,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
else: # Text Paragraph else: # Text Paragraph
if text.startswith("["): # Special Command if text.startswith("["): # Special Command
sText = text.rstrip() sText = text.rstrip().lower()
if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"): if sText in ("[newpage]", "[new page]", "[vspace]"):
self.setFormat(0, len(text), self._hStyles["keyword"]) self.setFormat(0, len(text), self._hStyles["code"])
return return
elif sText.startswith("[vspace:") and sText.endswith("]"):
elif sText.startswith("[VSPACE:") and sText.endswith("]"):
tLen = len(sText) tLen = len(sText)
tVal = checkInt(sText[8:-1], 0) tVal = checkInt(sText[8:-1], 0)
cVal = "codevalue" if tVal > 0 else "codeinval" cVal = "codevalue" if tVal > 0 else "codeinval"
self.setFormat(0, 8, self._hStyles["keyword"]) self.setFormat(0, 8, self._hStyles["code"])
self.setFormat(8, tLen-9, self._hStyles[cVal]) self.setFormat(8, tLen-9, self._hStyles[cVal])
self.setFormat(tLen-1, tLen, self._hStyles["keyword"]) self.setFormat(tLen-1, tLen, self._hStyles["code"])
return return
# Regular Text # Regular Text
+2
View File
@@ -97,6 +97,7 @@ class GuiTheme:
self.colDialD = [0, 0, 0] self.colDialD = [0, 0, 0]
self.colDialS = [0, 0, 0] self.colDialS = [0, 0, 0]
self.colHidden = [0, 0, 0] self.colHidden = [0, 0, 0]
self.colCode = [0, 0, 0]
self.colKey = [0, 0, 0] self.colKey = [0, 0, 0]
self.colVal = [0, 0, 0] self.colVal = [0, 0, 0]
self.colSpell = [0, 0, 0] self.colSpell = [0, 0, 0]
@@ -316,6 +317,7 @@ class GuiTheme:
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes") self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes") self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
self.colHidden = self._parseColour(confParser, cnfSec, "hidden") self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
self.colKey = self._parseColour(confParser, cnfSec, "keyword") self.colKey = self._parseColour(confParser, cnfSec, "keyword")
self.colVal = self._parseColour(confParser, cnfSec, "value") self.colVal = self._parseColour(confParser, cnfSec, "value")
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline") self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
+4 -2
View File
@@ -1,8 +1,8 @@
%%~name: Making a Scene %%~name: Making a Scene
%%~path: 6a2d6d5f4f401/636b6aa9b697b %%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5 %%~hash: d7920fe95fb48140278ae160fe7c3c1ce530072c
%%~date: Unknown/2023-09-07 19:00:10 %%~date: Unknown/2023-10-20 22:57:56
### Making a Scene ### Making a Scene
@pov: Jane @pov: Jane
@@ -13,6 +13,8 @@ A scene is defined by a level three heading, like the one at the top of this pag
Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesnt show it correctly, the export tool will not either. Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesnt show it correctly, the export tool will not either.
For special formatting aside from standard emphasis, you can use “shortcodes”. Here we have super[sup]script[/sup] and sub[sub]script[/sub], and [b]part[/b]ial bold too, which ends in the middle of a word. The shortcodes also support [u]underline[/u].
In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings. If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings.
+5 -5
View File
@@ -1,11 +1,11 @@
%%~name: Page %%~name: Page
%%~path: 7031beac91f75/974e400180a99 %%~path: 7031beac91f75/974e400180a99
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139 %%~hash: a0b0719e19763bf505bcf797bea0968c505c9b4a
%%~date: Unknown/2023-08-25 16:51:54 %%~date: Unknown/2023-11-06 11:19:33
[NEW PAGE] [newpage]
[VSPACE:2] [vspace:2]
This is a plain page with some text on it. This is a plain page with some text on it.
If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. The above code adds two empty paragraphs before the text starts. If you want the text to start on a fresh page, add the [newpage] code above the text. You can also add empty paragraphs with the [vspace] code. The above code adds two empty paragraphs before the text starts.
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-11-05 18:54:42"> <novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-11-06 11:44:21">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1532" autoCount="238" editTime="75633"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1609" autoCount="255" editTime="81165">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
@@ -36,7 +36,7 @@
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry> <entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance> </importance>
</settings> </settings>
<content items="27" novelWords="954" notesWords="409"> <content items="27" novelWords="989" notesWords="409">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL"> <item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" /> <meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name> <name status="sc24b8f" import="ia857f0">Novel</name>
@@ -46,7 +46,7 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name> <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item> </item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H0" charCount="251" wordCount="50" paraCount="2" cursorPos="277" /> <meta expanded="no" heading="H0" charCount="269" wordCount="51" paraCount="3" cursorPos="275" />
<name status="sf12341" import="ia857f0" active="yes">Page</name> <name status="sf12341" import="ia857f0" active="yes">Page</name>
</item> </item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name> <name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2686" wordCount="479" paraCount="14" cursorPos="19" /> <meta expanded="no" heading="H3" charCount="2901" wordCount="513" paraCount="15" cursorPos="749" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text"> <office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta> <office:meta>
<meta:creation-date>2023-07-19T23:55:29</meta:creation-date> <meta:creation-date>2023-10-20T22:51:30</meta:creation-date>
<meta:generator>novelWriter/2.1-beta1</meta:generator> <meta:generator>novelWriter/2.2-alpha1</meta:generator>
<meta:initial-creator>Jane Smith</meta:initial-creator> <meta:initial-creator>Jane Smith</meta:initial-creator>
<meta:editing-cycles>1234</meta:editing-cycles> <meta:editing-cycles>1234</meta:editing-cycles>
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration> <meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
<dc:title>Test Project</dc:title> <dc:title>Test Project</dc:title>
<dc:date>2023-07-19T23:55:29</dc:date> <dc:date>2023-10-20T22:51:30</dc:date>
<dc:creator>Jane Smith</dc:creator> <dc:creator>Jane Smith</dc:creator>
</office:meta> </office:meta>
<office:font-face-decls> <office:font-face-decls>
@@ -64,7 +64,7 @@
</office:styles> </office:styles>
<office:automatic-styles> <office:automatic-styles>
<style:page-layout style:name="PM1"> <style:page-layout style:name="PM1">
<style:page-layout-properties fo:page-width="21.0cm" fo:page-height="29.7cm" fo:margin-top="2.000cm" fo:margin-bottom="2.000cm" fo:margin-left="2.000cm" fo:margin-right="2.000cm" fo:print-orientation="portrait" /> <style:page-layout-properties fo:page-width="14.800cm" fo:page-height="21.000cm" fo:margin-top="2.000cm" fo:margin-bottom="1.800cm" fo:margin-left="1.700cm" fo:margin-right="1.500cm" fo:print-orientation="portrait" />
<style:header-style> <style:header-style>
<style:header-footer-properties fo:min-height="0.600cm" fo:margin-left="0.000cm" fo:margin-right="0.000cm" fo:margin-bottom="0.500cm" /> <style:header-footer-properties fo:min-height="0.600cm" fo:margin-left="0.000cm" fo:margin-right="0.000cm" fo:margin-bottom="0.500cm" />
</style:header-style> </style:header-style>
+26 -1
View File
@@ -1281,7 +1281,7 @@ def testCoreIndex_CountWords():
assert wC == 9 assert wC == 9
assert pC == 4 assert pC == 4
# Formatting Codes # Formatting Codes, Upper Case (Old Implementation)
cC, wC, pC = countWords(( cC, wC, pC = countWords((
"Some text\n\n" "Some text\n\n"
"[NEWPAGE]\n\n" "[NEWPAGE]\n\n"
@@ -1297,4 +1297,29 @@ def testCoreIndex_CountWords():
assert wC == 13 assert wC == 13
assert pC == 5 assert pC == 5
# Formatting Codes, Lower Case (Current Implementation)
cC, wC, pC = countWords((
"Some text\n\n"
"[newpage]\n\n"
"more text\n\n"
"[new page]]\n\n"
"even more text\n\n"
"[vspace]\n\n"
"and some final text\n\n"
"[vspace:4]\n\n"
"THE END\n\n"
))
assert cC == 58
assert wC == 13
assert pC == 5
# Check ShortCodes
cC, wC, pC = countWords((
"Text with [b]bold[/b] text and padded [b] bold [/b] text.\n\n"
"Text with [b][i] nested [/i] emphasis [/b] in it.\n\n"
))
assert cC == 78
assert wC == 14
assert pC == 2
# END Test testCoreIndex_CountWords # END Test testCoreIndex_CountWords
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -81,8 +81,8 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
# Paragraphs # Paragraphs
# ========== # ==========
# Text for GitHub Markdown # Text for Extended Markdown
theMD.setGitHubMarkdown() theMD.setExtendedMarkdown()
theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD.tokenizeText() theMD.tokenizeText()
theMD.doConvert() theMD.doConvert()
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -600,15 +600,15 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsNewPage.activate(QAction.Trigger) nwGUI.mainMenu.aInsNewPage.activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "[NEW PAGE]\n### Stuff\n" assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n"
nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.Trigger) nwGUI.mainMenu.aInsVSpaceS.activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "[VSPACE]\n### Stuff\n" assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n"
nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceM.activate(QAction.Trigger) nwGUI.mainMenu.aInsVSpaceM.activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "[VSPACE:2]\n### Stuff\n" assert nwGUI.docEditor.getText() == "[vspace:2]\n### Stuff\n"
nwGUI.docEditor.clear() nwGUI.docEditor.clear()