Add sub/superscript and partial word emphasis (#1540)
This commit is contained in:
@@ -159,6 +159,7 @@ A syntax theme ``.conf`` file consists of the follwing settings:
|
||||
doublequotes = 0, 0, 0
|
||||
singlequotes = 0, 0, 0
|
||||
hidden = 0, 0, 0
|
||||
shortcode = 0, 0, 0
|
||||
keyword = 0, 0, 0
|
||||
value = 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
|
||||
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,
|
||||
|
||||
.. 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
|
||||
singlequotes = 136, 200, 0
|
||||
hidden = 150, 150, 150
|
||||
shortcode = 0, 155, 200
|
||||
keyword = 200, 46, 0
|
||||
value = 184, 200, 0
|
||||
spellcheckline = 200, 46, 0
|
||||
|
||||
@@ -17,6 +17,7 @@ straightquotes = 200, 0, 0
|
||||
doublequotes = 0, 0, 200
|
||||
singlequotes = 0, 0, 200
|
||||
hidden = 100, 100, 100
|
||||
shortcode = 0, 100, 0
|
||||
keyword = 200, 50, 50
|
||||
value = 50, 150, 50
|
||||
spellcheckline = 200, 0, 0
|
||||
|
||||
@@ -17,6 +17,7 @@ straightquotes = 200, 200, 200
|
||||
doublequotes = 200, 200, 200
|
||||
singlequotes = 200, 200, 200
|
||||
hidden = 150, 150, 150
|
||||
shortcode = 225, 225, 225
|
||||
keyword = 225, 225, 225
|
||||
value = 200, 200, 200
|
||||
spellcheckline = 200, 46, 0
|
||||
|
||||
@@ -17,6 +17,7 @@ straightquotes = 20, 20, 20
|
||||
doublequotes = 20, 20, 20
|
||||
singlequotes = 20, 20, 20
|
||||
hidden = 100, 100, 100
|
||||
shortcode = 0, 0, 0
|
||||
keyword = 0, 0, 0
|
||||
value = 20, 20, 20
|
||||
spellcheckline = 200, 0, 0
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 222, 61, 58
|
||||
doublequotes = 8, 145, 106
|
||||
singlequotes = 255, 235, 149
|
||||
hidden = 152, 159, 177
|
||||
shortcode = 40, 142, 215
|
||||
keyword = 222, 61, 58
|
||||
value = 150, 74, 193
|
||||
spellcheckline = 222, 61, 58
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 247, 140, 108
|
||||
doublequotes = 173, 219, 103
|
||||
singlequotes = 255, 235, 149
|
||||
hidden = 99, 119, 119
|
||||
shortcode = 130, 170, 255
|
||||
keyword = 247, 140, 108
|
||||
value = 199, 146, 234
|
||||
spellcheckline = 247, 140, 108
|
||||
|
||||
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
|
||||
doublequotes = 42, 161, 152
|
||||
singlequotes = 42, 161, 152
|
||||
hidden = 147, 161, 161
|
||||
shortcode = 147, 161, 161
|
||||
keyword = 133, 153, 0
|
||||
value = 203, 75, 22
|
||||
spellcheckline = 203, 75, 22
|
||||
|
||||
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
|
||||
doublequotes = 42, 161, 152
|
||||
singlequotes = 42, 161, 152
|
||||
hidden = 88, 110, 117
|
||||
shortcode = 88, 110, 117
|
||||
keyword = 133, 153, 0
|
||||
value = 203, 75, 22
|
||||
spellcheckline = 203, 75, 22
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 240, 40, 41
|
||||
doublequotes = 113, 140, 0
|
||||
singlequotes = 234, 183, 0
|
||||
hidden = 142, 144, 140
|
||||
shortcode = 66, 113, 174
|
||||
keyword = 240, 40, 41
|
||||
value = 137, 89, 168
|
||||
spellcheckline = 240, 40, 41
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 204, 102, 102
|
||||
doublequotes = 181, 189, 104
|
||||
singlequotes = 240, 198, 116
|
||||
hidden = 150, 152, 150
|
||||
shortcode = 129, 162, 190
|
||||
keyword = 204, 102, 102
|
||||
value = 178, 148, 187
|
||||
spellcheckline = 204, 102, 102
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 255, 157, 164
|
||||
doublequotes = 209, 241, 169
|
||||
singlequotes = 255, 238, 173
|
||||
hidden = 114, 133, 183
|
||||
shortcode = 187, 218, 255
|
||||
keyword = 255, 157, 164
|
||||
value = 235, 187, 255
|
||||
spellcheckline = 255, 157, 164
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 213, 78, 83
|
||||
doublequotes = 185, 202, 74
|
||||
singlequotes = 231, 197, 71
|
||||
hidden = 150, 152, 150
|
||||
shortcode = 122, 166, 218
|
||||
keyword = 213, 78, 83
|
||||
value = 195, 151, 216
|
||||
spellcheckline = 213, 78, 83
|
||||
|
||||
@@ -37,6 +37,7 @@ straightquotes = 242, 119, 122
|
||||
doublequotes = 153, 204, 153
|
||||
singlequotes = 255, 204, 102
|
||||
hidden = 153, 153, 153
|
||||
shortcode = 102, 153, 204
|
||||
keyword = 242, 119, 122
|
||||
value = 204, 153, 204
|
||||
spellcheckline = 242, 119, 122
|
||||
|
||||
@@ -23,6 +23,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
|
||||
|
||||
from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
|
||||
@@ -59,6 +61,11 @@ class nwRegEx:
|
||||
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_EB = 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
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ class NWBuildDocument:
|
||||
filtered = self._setupBuild(makeObj)
|
||||
|
||||
if extendedMd:
|
||||
makeObj.setGitHubMarkdown()
|
||||
makeObj.setExtendedMarkdown()
|
||||
else:
|
||||
makeObj.setStandardMarkdown()
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ from pathlib import Path
|
||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from novelwriter.error import logException
|
||||
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
|
||||
from novelwriter.core.item import NWItem
|
||||
@@ -1229,6 +1229,10 @@ def countWords(text: str) -> tuple[int, int, int]:
|
||||
if nwUnicode.U_EMDASH in text:
|
||||
text = text.replace(nwUnicode.U_EMDASH, " ")
|
||||
|
||||
# Strip shortcodes
|
||||
if "[" in text:
|
||||
text = nwRegEx.RX_SC.sub("", text)
|
||||
|
||||
for line in text.splitlines():
|
||||
|
||||
countPara = True
|
||||
@@ -1241,9 +1245,10 @@ def countWords(text: str) -> tuple[int, int, int]:
|
||||
continue
|
||||
|
||||
if line[0] == "[":
|
||||
if line.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
|
||||
check = line.lower()
|
||||
if check.startswith(("[newpage]", "[new page]", "[vspace]")):
|
||||
continue
|
||||
elif line.startswith("[VSPACE:") and line.endswith("]"):
|
||||
elif check.startswith("[vspace:") and line.endswith("]"):
|
||||
continue
|
||||
|
||||
elif line[0] == "#":
|
||||
|
||||
+23
-23
@@ -31,7 +31,7 @@ from pathlib import Path
|
||||
|
||||
from novelwriter import CONFIG
|
||||
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.tokenizer import Tokenizer, stripEscape
|
||||
|
||||
@@ -131,6 +131,8 @@ class ToHtml(Tokenizer):
|
||||
self.FMT_I_E: "</i>",
|
||||
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
|
||||
self.FMT_D_E: "</span>",
|
||||
self.FMT_U_B: "<u>",
|
||||
self.FMT_U_E: "</u>",
|
||||
}
|
||||
else:
|
||||
htmlTags = { # HTML5 (for export)
|
||||
@@ -140,8 +142,15 @@ class ToHtml(Tokenizer):
|
||||
self.FMT_I_E: "</em>",
|
||||
self.FMT_D_B: "<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:
|
||||
# For story files, we bump the titles one level up
|
||||
h1Cl = " class='title'"
|
||||
@@ -172,18 +181,16 @@ class ToHtml(Tokenizer):
|
||||
for c in tText:
|
||||
if c == "<":
|
||||
cText.append("<")
|
||||
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
|
||||
elif c == ">":
|
||||
cText.append(">")
|
||||
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
|
||||
else:
|
||||
cText.append(c)
|
||||
i += 1
|
||||
|
||||
tText = "".join(cText)
|
||||
|
||||
else:
|
||||
# If we don't have formatting, we can do a plain replace
|
||||
tText = tText.replace("<", "<").replace(">", ">")
|
||||
@@ -275,8 +282,8 @@ class ToHtml(Tokenizer):
|
||||
tTemp = tText
|
||||
if pStyle is None:
|
||||
pStyle = hStyle
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
|
||||
for pos, fmt in reversed(tFormat):
|
||||
tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
|
||||
para.append(stripEscape(tTemp.rstrip()))
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
@@ -467,25 +474,18 @@ class ToHtml(Tokenizer):
|
||||
def _formatKeywords(self, text: str) -> str:
|
||||
"""Apply HTML formatting to keywords."""
|
||||
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 ""
|
||||
|
||||
result = ""
|
||||
tags = []
|
||||
if bits[0] in nwLabels.KEY_NAME:
|
||||
result += f"<span class='tags'>{nwLabels.KEY_NAME[bits[0]]}:</span> "
|
||||
if len(bits) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
|
||||
result = f"<span class='tags'>{trConst(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
||||
if len(bits) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
|
||||
else:
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
|
||||
else:
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
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)
|
||||
result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from operator import itemgetter
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
@@ -43,15 +42,14 @@ from novelwriter.core.project import NWProject
|
||||
|
||||
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:
|
||||
"""Helper function to strip escaped Markdown characters from
|
||||
paragraph text.
|
||||
"""
|
||||
"""Strip escaped Markdown characters from paragraph text."""
|
||||
if "\\" in text:
|
||||
# Checking first is slightly slower when there are escaped
|
||||
# characters in the text, but significantly faster when not
|
||||
return text.replace(r"\*", "*").replace(r"\~", "~").replace(r"\_", "_")
|
||||
return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text)
|
||||
return text
|
||||
|
||||
|
||||
@@ -65,12 +63,18 @@ class Tokenizer(ABC):
|
||||
"""
|
||||
|
||||
# In-Text Format
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_D_B = 5 # Begin strikeout
|
||||
FMT_D_E = 6 # End strikeout
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_D_B = 5 # Begin 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
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
@@ -166,6 +170,24 @@ class Tokenizer(ABC):
|
||||
# Cached Translations
|
||||
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
|
||||
|
||||
##
|
||||
@@ -387,19 +409,12 @@ class Tokenizer(ABC):
|
||||
4: The internal formatting map of the text, self.FMT_*
|
||||
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 = []
|
||||
tmpMarkdown = []
|
||||
nHead = 0
|
||||
breakNext = False
|
||||
for aLine in self._text.splitlines():
|
||||
sLine = aLine.strip()
|
||||
sLine = aLine.strip().lower()
|
||||
|
||||
# Check for blank lines
|
||||
if len(sLine) == 0:
|
||||
@@ -422,18 +437,21 @@ class Tokenizer(ABC):
|
||||
|
||||
if aLine[0] == "[":
|
||||
# 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
|
||||
continue
|
||||
|
||||
elif sLine == "[VSPACE]":
|
||||
elif sLine == "[vspace]":
|
||||
self._tokens.append(
|
||||
(self.T_SKIP, nHead, "", None, sAlign)
|
||||
)
|
||||
continue
|
||||
|
||||
elif sLine.startswith("[VSPACE:") and sLine.endswith("]"):
|
||||
elif sLine.startswith("[vspace:") and sLine.endswith("]"):
|
||||
nSkip = checkInt(sLine[8:-1], 0)
|
||||
if nSkip >= 1:
|
||||
self._tokens.append(
|
||||
@@ -445,7 +463,7 @@ class Tokenizer(ABC):
|
||||
]
|
||||
continue
|
||||
|
||||
elif aLine[0] == "%":
|
||||
if aLine[0] == "%":
|
||||
cLine = aLine[1:].lstrip()
|
||||
synTag = cLine[:9].lower()
|
||||
if synTag == "synopsis:":
|
||||
@@ -570,23 +588,10 @@ class Tokenizer(ABC):
|
||||
if indRight:
|
||||
sAlign |= self.A_IND_R
|
||||
|
||||
# Otherwise we use RegEx to find formatting tags within a line of text
|
||||
fmtPos = []
|
||||
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))
|
||||
# Process formats
|
||||
tLine, fmtPos = self._extractFormats(aLine)
|
||||
self._tokens.append((
|
||||
self.T_TEXT, nHead, aLine, fmtPos, sAlign
|
||||
self.T_TEXT, nHead, tLine, fmtPos, sAlign
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
@@ -763,6 +768,45 @@ class Tokenizer(ABC):
|
||||
json.dump(data, fObj, indent=2)
|
||||
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
|
||||
|
||||
|
||||
|
||||
+24
-12
@@ -27,7 +27,7 @@ import logging
|
||||
|
||||
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.tokenizer import Tokenizer
|
||||
|
||||
@@ -38,12 +38,12 @@ class ToMarkdown(Tokenizer):
|
||||
"""Core: Markdown Document Writer
|
||||
|
||||
Extend the Tokenizer class to writer Markdown output. It supports
|
||||
both Standard Markdown and GitHub Flavour Markdown (Extended). The
|
||||
class also supports concatenating novelWriter markup files.
|
||||
both Standard Markdown and Extended Markdown. The class also
|
||||
supports concatenating novelWriter markup files.
|
||||
"""
|
||||
|
||||
M_STD = 0 # Standard Markdown
|
||||
M_GH = 1 # GitHub Markdown
|
||||
M_EXT = 1 # Extended Markdown
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
super().__init__(project)
|
||||
@@ -69,9 +69,9 @@ class ToMarkdown(Tokenizer):
|
||||
self._genMode = self.M_STD
|
||||
return
|
||||
|
||||
def setGitHubMarkdown(self) -> None:
|
||||
"""Set the converter to use GitHub Markdown formatting."""
|
||||
self._genMode = self.M_GH
|
||||
def setExtendedMarkdown(self) -> None:
|
||||
"""Set the converter to use Extended Markdown formatting."""
|
||||
self._genMode = self.M_EXT
|
||||
return
|
||||
|
||||
##
|
||||
@@ -85,7 +85,7 @@ class ToMarkdown(Tokenizer):
|
||||
def doConvert(self) -> None:
|
||||
"""Convert the list of text tokens into a Markdown document."""
|
||||
if self._genMode == self.M_STD:
|
||||
# Standard
|
||||
# Standard Markdown
|
||||
mdTags = {
|
||||
self.FMT_B_B: "**",
|
||||
self.FMT_B_E: "**",
|
||||
@@ -93,9 +93,15 @@ class ToMarkdown(Tokenizer):
|
||||
self.FMT_I_E: "_",
|
||||
self.FMT_D_B: "",
|
||||
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:
|
||||
# GitHub
|
||||
# Extended Markdown
|
||||
mdTags = {
|
||||
self.FMT_B_B: "**",
|
||||
self.FMT_B_E: "**",
|
||||
@@ -103,6 +109,12 @@ class ToMarkdown(Tokenizer):
|
||||
self.FMT_I_E: "_",
|
||||
self.FMT_D_B: "~~",
|
||||
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 = ""
|
||||
@@ -150,8 +162,8 @@ class ToMarkdown(Tokenizer):
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
||||
for pos, fmt in reversed(tFormat):
|
||||
tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}"
|
||||
para.append(tTemp.rstrip())
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
@@ -197,7 +209,7 @@ class ToMarkdown(Tokenizer):
|
||||
|
||||
result = ""
|
||||
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:
|
||||
result += ", ".join(bits[1:])
|
||||
|
||||
|
||||
+126
-96
@@ -29,6 +29,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from typing import Sequence
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
@@ -36,7 +37,7 @@ from datetime import datetime
|
||||
|
||||
from novelwriter import __version__
|
||||
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.tokenizer import Tokenizer, stripEscape
|
||||
|
||||
@@ -82,11 +83,17 @@ TAG_STNM = _mkTag("text", "style-name")
|
||||
X_BLD = 0x01 # Bold format
|
||||
X_ITA = 0x02 # Italic format
|
||||
X_DEL = 0x04 # Strikethrough format
|
||||
X_UND = 0x08 # Underline format
|
||||
X_SUP = 0x10 # Superscript
|
||||
X_SUB = 0x20 # Subscript
|
||||
|
||||
# Formatting Masks
|
||||
M_BLD = ~X_BLD
|
||||
M_ITA = ~X_ITA
|
||||
M_DEL = ~X_DEL
|
||||
M_UND = ~X_UND
|
||||
M_SUP = ~X_SUP
|
||||
M_SUB = ~X_SUB
|
||||
|
||||
|
||||
class ToOdt(Tokenizer):
|
||||
@@ -385,17 +392,8 @@ class ToOdt(Tokenizer):
|
||||
"""Convert the list of text tokens into XML elements."""
|
||||
self._result = "" # Not used, but cleared just in case
|
||||
|
||||
odtTags = {
|
||||
self.FMT_B_B: "_B", # Bold open format
|
||||
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 = []
|
||||
pFmt = []
|
||||
pText = []
|
||||
pStyle = None
|
||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
@@ -429,19 +427,21 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# Process Text Types
|
||||
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:
|
||||
pStyle.setTextAlign("left")
|
||||
|
||||
if len(para) > 0 and pStyle is not None:
|
||||
tTemp = "\n".join(para)
|
||||
fTemp = " ".join(fmt)
|
||||
tTxt = tTemp.rstrip()
|
||||
tFmt = fTemp[:len(tTxt)]
|
||||
self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
|
||||
if len(pText) > 0 and pStyle is not None:
|
||||
tTxt = ""
|
||||
tFmt = []
|
||||
for nText, nFmt in zip(pText, pFmt):
|
||||
tLen = len(tTxt)
|
||||
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 = []
|
||||
para = []
|
||||
pFmt = []
|
||||
pText = []
|
||||
pStyle = None
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
@@ -477,15 +477,8 @@ class ToOdt(Tokenizer):
|
||||
elif tType == self.T_TEXT:
|
||||
if pStyle is None:
|
||||
pStyle = oStyle
|
||||
|
||||
tFmt = " "*len(tText)
|
||||
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)
|
||||
pText.append(tText)
|
||||
pFmt.append(tFormat)
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
tTemp, fTemp = self._formatSynopsis(tText)
|
||||
@@ -559,46 +552,39 @@ class ToOdt(Tokenizer):
|
||||
# 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."""
|
||||
sSynop = self._localLookup("Synopsis")
|
||||
rTxt = "**{0}:** {1}".format(sSynop, text)
|
||||
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(text))
|
||||
name = self._localLookup("Synopsis")
|
||||
rTxt = f"{name}: {text}"
|
||||
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
||||
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."""
|
||||
sComm = self._localLookup("Comment")
|
||||
rTxt = "**{0}:** {1}".format(sComm, text)
|
||||
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(text))
|
||||
name = self._localLookup("Comment")
|
||||
rTxt = f"{name}: {text}"
|
||||
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
||||
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."""
|
||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
||||
if not valid or not bits:
|
||||
return "", ""
|
||||
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
|
||||
return "", []
|
||||
|
||||
rTxt = ""
|
||||
rFmt = ""
|
||||
if bits[0] in nwLabels.KEY_NAME:
|
||||
text = nwLabels.KEY_NAME[bits[0]]
|
||||
rTxt += "**{0}:** ".format(text)
|
||||
rFmt += "_B{0} b_ ".format(" "*len(text))
|
||||
if len(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))
|
||||
rTxt = f"{trConst(nwLabels.KEY_NAME[bits[0]])}: "
|
||||
rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
|
||||
if len(bits) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
rTxt += bits[1]
|
||||
else:
|
||||
rTxt += ", ".join(bits[1:])
|
||||
|
||||
return rTxt, rFmt
|
||||
|
||||
def _addTextPar(
|
||||
self, styleName: str, oStyle: ODTParagraphStyle, tText: str, tFmt: str = "",
|
||||
isHead: bool = False, oLevel: str | None = None
|
||||
self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
|
||||
tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
|
||||
) -> None:
|
||||
"""Add a text paragraph to the text XML element."""
|
||||
tAttr = {}
|
||||
@@ -616,58 +602,59 @@ class ToOdt(Tokenizer):
|
||||
if not tText:
|
||||
return
|
||||
|
||||
##
|
||||
# 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
|
||||
# Loop Over Fragments
|
||||
# ===================
|
||||
|
||||
parProc = XMLParagraph(xElem)
|
||||
|
||||
for i, c in enumerate(tText):
|
||||
pErr = 0
|
||||
xFmt = 0x00
|
||||
tFrag = ""
|
||||
fLast = 0
|
||||
for fPos, fFmt in tFmt:
|
||||
|
||||
if tFmt[i] == " ":
|
||||
tTemp += c
|
||||
elif tFmt[i] == "_":
|
||||
continue
|
||||
elif tFmt[i] == "B":
|
||||
# Add the text up to the current fragment
|
||||
if tFrag := tText[fLast:fPos]:
|
||||
if xFmt == 0x00:
|
||||
parProc.appendText(tFrag)
|
||||
else:
|
||||
parProc.appendSpan(tFrag, self._textStyle(xFmt))
|
||||
|
||||
# Calculate the change of format
|
||||
if fFmt == self.FMT_B_B:
|
||||
xFmt |= X_BLD
|
||||
elif tFmt[i] == "b":
|
||||
elif fFmt == self.FMT_B_E:
|
||||
xFmt &= M_BLD
|
||||
elif tFmt[i] == "I":
|
||||
elif fFmt == self.FMT_I_B:
|
||||
xFmt |= X_ITA
|
||||
elif tFmt[i] == "i":
|
||||
elif fFmt == self.FMT_I_E:
|
||||
xFmt &= M_ITA
|
||||
elif tFmt[i] == "S":
|
||||
elif fFmt == self.FMT_D_B:
|
||||
xFmt |= X_DEL
|
||||
elif tFmt[i] == "s":
|
||||
elif fFmt == self.FMT_D_E:
|
||||
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:
|
||||
pErr += 1
|
||||
|
||||
if xFmt != pFmt:
|
||||
if pFmt == 0x00:
|
||||
parProc.appendText(tTemp)
|
||||
tTemp = ""
|
||||
else:
|
||||
parProc.appendSpan(tTemp, self._textStyle(pFmt))
|
||||
tTemp = ""
|
||||
fLast = fPos
|
||||
|
||||
pFmt = xFmt
|
||||
|
||||
# Save what remains in the buffer
|
||||
if pFmt == 0x00:
|
||||
parProc.appendText(tTemp)
|
||||
else:
|
||||
parProc.appendSpan(tTemp, self._textStyle(pFmt))
|
||||
if tFrag := tText[fLast:]:
|
||||
if xFmt == 0x00:
|
||||
parProc.appendText(tFrag)
|
||||
else:
|
||||
parProc.appendSpan(tFrag, self._textStyle(xFmt))
|
||||
|
||||
if pErr > 0:
|
||||
self._errData.append("Unknown format tag encountered")
|
||||
@@ -713,6 +700,14 @@ class ToOdt(Tokenizer):
|
||||
if hFmt & X_DEL:
|
||||
newStyle.setStrikeStyle("solid")
|
||||
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)
|
||||
|
||||
@@ -1256,16 +1251,23 @@ class ODTTextStyle:
|
||||
"""
|
||||
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
||||
VALID_STYLE = ["normal", "inherit", "italic"]
|
||||
VALID_POS = ["super", "sub"]
|
||||
VALID_LSTYLE = ["none", "solid"]
|
||||
VALID_LTYPE = ["none", "single", "double"]
|
||||
VALID_LTYPE = ["single", "double"]
|
||||
VALID_LWIDTH = ["auto"]
|
||||
VALID_LCOL = ["font-color"]
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Text Attributes
|
||||
self._tAttr = {
|
||||
"font-weight": ["fo", None],
|
||||
"font-style": ["fo", None],
|
||||
"text-position": ["style", None],
|
||||
"text-line-through-style": ["style", None],
|
||||
"text-line-through-type": ["style", None],
|
||||
"text-underline-style": ["style", None],
|
||||
"text-underline-width": ["style", None],
|
||||
"text-underline-color": ["style", None],
|
||||
}
|
||||
return
|
||||
|
||||
@@ -1287,6 +1289,13 @@ class ODTTextStyle:
|
||||
self._tAttr["font-style"][1] = None
|
||||
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:
|
||||
if value in self.VALID_LSTYLE:
|
||||
self._tAttr["text-line-through-style"][1] = value
|
||||
@@ -1301,6 +1310,27 @@ class ODTTextStyle:
|
||||
self._tAttr["text-line-through-type"][1] = None
|
||||
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
|
||||
##
|
||||
|
||||
@@ -55,6 +55,7 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
|
||||
from novelwriter.common import minmax, transferCase
|
||||
from novelwriter.constants import nwKeyWords, nwUnicode
|
||||
from novelwriter.core.index import countWords
|
||||
from novelwriter.core.document import NWDocument
|
||||
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||
from novelwriter.gui.editordocument import GuiTextDocument
|
||||
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
|
||||
@@ -408,12 +409,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
|
||||
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:
|
||||
"""Replace the text of the current document with the provided
|
||||
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
|
||||
is saved.
|
||||
"""
|
||||
if self._nwDocument is None:
|
||||
logger.error("No document open")
|
||||
return
|
||||
SHARED.info(
|
||||
"<br>".join([
|
||||
self.tr("Document Details"),
|
||||
"–"*40,
|
||||
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
|
||||
)
|
||||
if isinstance(self._nwDocument, NWDocument):
|
||||
SHARED.info(
|
||||
"<br>".join([
|
||||
self.tr("Document Details"),
|
||||
"–"*40,
|
||||
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
|
||||
)
|
||||
return
|
||||
|
||||
def insertText(self, insert: str | nwDocInsert) -> bool:
|
||||
@@ -775,15 +768,15 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
newBlock = True
|
||||
goAfter = True
|
||||
elif insert == nwDocInsert.NEW_PAGE:
|
||||
text = "[NEW PAGE]"
|
||||
text = "[newpage]"
|
||||
newBlock = True
|
||||
goAfter = False
|
||||
elif insert == nwDocInsert.VSPACE_S:
|
||||
text = "[VSPACE]"
|
||||
text = "[vspace]"
|
||||
newBlock = True
|
||||
goAfter = False
|
||||
elif insert == nwDocInsert.VSPACE_M:
|
||||
text = "[VSPACE:2]"
|
||||
text = "[vspace:2]"
|
||||
newBlock = True
|
||||
goAfter = False
|
||||
else:
|
||||
|
||||
@@ -39,7 +39,7 @@ from novelwriter.constants import nwRegEx, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b")
|
||||
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
|
||||
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._colDialD = QColor(0, 0, 0)
|
||||
self._colDialS = QColor(0, 0, 0)
|
||||
self._colHidden = QColor(0, 0, 0)
|
||||
self._colCode = QColor(0, 0, 0)
|
||||
self._colKey = QColor(0, 0, 0)
|
||||
self._colVal = QColor(0, 0, 0)
|
||||
self._colSpell = QColor(0, 0, 0)
|
||||
@@ -95,6 +96,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._colDialD = QColor(*SHARED.theme.colDialD)
|
||||
self._colDialS = QColor(*SHARED.theme.colDialS)
|
||||
self._colHidden = QColor(*SHARED.theme.colHidden)
|
||||
self._colCode = QColor(*SHARED.theme.colCode)
|
||||
self._colKey = QColor(*SHARED.theme.colKey)
|
||||
self._colVal = QColor(*SHARED.theme.colVal)
|
||||
self._colSpell = QColor(*SHARED.theme.colSpell)
|
||||
@@ -127,6 +129,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
"dialogue3": self._makeFormat(self._colDialS),
|
||||
"replace": self._makeFormat(self._colRepTag),
|
||||
"hidden": self._makeFormat(self._colHidden),
|
||||
"code": self._makeFormat(self._colCode),
|
||||
"keyword": self._makeFormat(self._colKey),
|
||||
"modifier": self._makeFormat(self._colMod),
|
||||
"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
|
||||
self._hRules.append((
|
||||
r"(^>{1,2}|<{1,2}$)", {
|
||||
@@ -347,18 +366,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
else: # Text Paragraph
|
||||
|
||||
if text.startswith("["): # Special Command
|
||||
sText = text.rstrip()
|
||||
if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"):
|
||||
self.setFormat(0, len(text), self._hStyles["keyword"])
|
||||
sText = text.rstrip().lower()
|
||||
if sText in ("[newpage]", "[new page]", "[vspace]"):
|
||||
self.setFormat(0, len(text), self._hStyles["code"])
|
||||
return
|
||||
|
||||
elif sText.startswith("[VSPACE:") and sText.endswith("]"):
|
||||
elif sText.startswith("[vspace:") and sText.endswith("]"):
|
||||
tLen = len(sText)
|
||||
tVal = checkInt(sText[8:-1], 0)
|
||||
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(tLen-1, tLen, self._hStyles["keyword"])
|
||||
self.setFormat(tLen-1, tLen, self._hStyles["code"])
|
||||
return
|
||||
|
||||
# Regular Text
|
||||
|
||||
@@ -97,6 +97,7 @@ class GuiTheme:
|
||||
self.colDialD = [0, 0, 0]
|
||||
self.colDialS = [0, 0, 0]
|
||||
self.colHidden = [0, 0, 0]
|
||||
self.colCode = [0, 0, 0]
|
||||
self.colKey = [0, 0, 0]
|
||||
self.colVal = [0, 0, 0]
|
||||
self.colSpell = [0, 0, 0]
|
||||
@@ -316,6 +317,7 @@ class GuiTheme:
|
||||
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
|
||||
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
|
||||
self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
|
||||
self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
|
||||
self.colKey = self._parseColour(confParser, cnfSec, "keyword")
|
||||
self.colVal = self._parseColour(confParser, cnfSec, "value")
|
||||
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
%%~name: Making a Scene
|
||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
|
||||
%%~date: Unknown/2023-09-07 19:00:10
|
||||
%%~hash: d7920fe95fb48140278ae160fe7c3c1ce530072c
|
||||
%%~date: Unknown/2023-10-20 22:57:56
|
||||
### Making a Scene
|
||||
|
||||
@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 doesn’t 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.”
|
||||
|
||||
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, let’s 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.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
%%~name: Page
|
||||
%%~path: 7031beac91f75/974e400180a99
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139
|
||||
%%~date: Unknown/2023-08-25 16:51:54
|
||||
[NEW PAGE]
|
||||
[VSPACE:2]
|
||||
%%~hash: a0b0719e19763bf505bcf797bea0968c505c9b4a
|
||||
%%~date: Unknown/2023-11-06 11:19:33
|
||||
[newpage]
|
||||
[vspace:2]
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?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">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1532" autoCount="238" editTime="75633">
|
||||
<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="1609" autoCount="255" editTime="81165">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
@@ -36,7 +36,7 @@
|
||||
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
|
||||
</importance>
|
||||
</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">
|
||||
<meta expanded="yes" />
|
||||
<name status="sc24b8f" import="ia857f0">Novel</name>
|
||||
@@ -46,7 +46,7 @@
|
||||
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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'?>
|
||||
<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>
|
||||
<meta:creation-date>2023-07-19T23:55:29</meta:creation-date>
|
||||
<meta:generator>novelWriter/2.1-beta1</meta:generator>
|
||||
<meta:creation-date>2023-10-20T22:51:30</meta:creation-date>
|
||||
<meta:generator>novelWriter/2.2-alpha1</meta:generator>
|
||||
<meta:initial-creator>Jane Smith</meta:initial-creator>
|
||||
<meta:editing-cycles>1234</meta:editing-cycles>
|
||||
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
|
||||
<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>
|
||||
</office:meta>
|
||||
<office:font-face-decls>
|
||||
@@ -64,7 +64,7 @@
|
||||
</office:styles>
|
||||
<office:automatic-styles>
|
||||
<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-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>
|
||||
|
||||
@@ -1281,7 +1281,7 @@ def testCoreIndex_CountWords():
|
||||
assert wC == 9
|
||||
assert pC == 4
|
||||
|
||||
# Formatting Codes
|
||||
# Formatting Codes, Upper Case (Old Implementation)
|
||||
cC, wC, pC = countWords((
|
||||
"Some text\n\n"
|
||||
"[NEWPAGE]\n\n"
|
||||
@@ -1297,4 +1297,29 @@ def testCoreIndex_CountWords():
|
||||
assert wC == 13
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,8 +81,8 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
|
||||
# Paragraphs
|
||||
# ==========
|
||||
|
||||
# Text for GitHub Markdown
|
||||
theMD.setGitHubMarkdown()
|
||||
# Text for Extended Markdown
|
||||
theMD.setExtendedMarkdown()
|
||||
theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||
theMD.tokenizeText()
|
||||
theMD.doConvert()
|
||||
|
||||
+390
-297
File diff suppressed because it is too large
Load Diff
@@ -600,15 +600,15 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||
|
||||
nwGUI.docEditor.setPlainText("### Stuff\n")
|
||||
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.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.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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user