Add shortcodes functionality

This commit is contained in:
Veronica Berglyd Olsen
2023-10-20 17:50:18 +02:00
parent 2214b7472b
commit 1558a11646
4 changed files with 72 additions and 77 deletions
+2 -9
View File
@@ -59,15 +59,8 @@ 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_IN = r"(?<![\\])(\[)(b|i|u|s|sup|sub)(:)(.+?)(?<![\\])(\])" FMT_SC = r"(?<!\\)(\[[\/\!]?(?i)(?:i|b|s|u|sup|sub)\])"
FMT_SV = r"(?<!\\)(\[(?i)(?:fn|footnote):)(.+?)(?<!\\)(\])"
# Format breakdown of inline codes from FMT_IN
INL_EI = r"(?<![\\])(\[i:)(.+?)(?<![\\])(\])"
INL_EB = r"(?<![\\])(\[b:)(.+?)(?<![\\])(\])"
INL_ST = r"(?<![\\])(\[s:)(.+?)(?<![\\])(\])"
INL_UN = r"(?<![\\])(\[u:)(.+?)(?<![\\])(\])"
INL_UP = r"(?<![\\])(\[sup:)(.+?)(?<![\\])(\])"
INL_DN = r"(?<![\\])(\[sub:)(.+?)(?<![\\])(\])"
# END Class nwRegEx # END Class nwRegEx
+3 -24
View File
@@ -174,28 +174,7 @@ class ToHtml(Tokenizer):
for tType, tLine, tText, tFormat, tStyle in self._tokens: for tType, tLine, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities # Replace < and > with HTML entities
if tFormat: tText = tText.replace("<", "&lt;").replace(">", "&gt;")
# If we have formatting, we must recompute the locations
cText = []
i = 0
for c in tText:
if c == "<":
cText.append("&lt;")
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat]
i += 4
elif c == ">":
cText.append("&gt;")
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c 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("<", "&lt;").replace(">", "&gt;")
# Styles # Styles
aStyle = [] aStyle = []
@@ -284,8 +263,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:
+65 -42
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,14 +42,7 @@ from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ESCAPES = { ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
r"\*": "*",
r"\~": "~",
r"\_": "_",
r"\[": "[",
r"\]": "]",
r"\ ": "",
}
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL) RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
@@ -178,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
## ##
@@ -399,26 +409,13 @@ 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]),
(QRegularExpression(nwRegEx.INL_EI), [None, self.FMT_I_B, None, self.FMT_I_E]),
(QRegularExpression(nwRegEx.INL_EB), [None, self.FMT_B_B, None, self.FMT_B_E]),
(QRegularExpression(nwRegEx.INL_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
(QRegularExpression(nwRegEx.INL_UN), [None, self.FMT_U_B, None, self.FMT_U_E]),
(QRegularExpression(nwRegEx.INL_UP), [None, self.FMT_SUP_B, None, self.FMT_SUP_E]),
(QRegularExpression(nwRegEx.INL_DN), [None, self.FMT_SUB_B, None, self.FMT_SUB_E]),
]
self._tokens = [] self._tokens = []
tmpMarkdown = [] tmpMarkdown = []
nLine = 0 nLine = 0
breakNext = False breakNext = False
for aLine in self._text.splitlines(): for aLine in self._text.splitlines():
nLine += 1 nLine += 1
sLine = aLine.strip() sLine = aLine.strip().lower()
# Check for blank lines # Check for blank lines
if len(sLine) == 0: if len(sLine) == 0:
@@ -445,17 +442,17 @@ class Tokenizer(ABC):
# reach a continue statement and must thefore proceed to # reach a continue statement and must thefore proceed to
# check other formats. # 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, nLine, "", None, sAlign) (self.T_SKIP, nLine, "", 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(
@@ -586,23 +583,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 regEx, keys in rxFormats:
rxThis = regEx.globalMatch(aLine, 0)
while rxThis.hasNext():
rxMatch = rxThis.next()
for n in range(1, len(keys)):
if keys[n] is not None:
xPos = rxMatch.capturedStart(n)
xLen = rxMatch.capturedLength(n)
fmtPos.append([xPos, xLen, keys[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, nLine, aLine, fmtPos, sAlign self.T_TEXT, nLine, tLine, fmtPos, sAlign
)) ))
if self._keepMarkdown: if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine) tmpMarkdown.append("%s\n" % aLine)
@@ -779,6 +763,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
+2 -2
View File
@@ -162,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: