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_EB = 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)(:)(.+?)(?<![\\])(\])"
# 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:)(.+?)(?<![\\])(\])"
FMT_SC = r"(?<!\\)(\[[\/\!]?(?i)(?:i|b|s|u|sup|sub)\])"
FMT_SV = r"(?<!\\)(\[(?i)(?:fn|footnote):)(.+?)(?<!\\)(\])"
# END Class nwRegEx
+3 -24
View File
@@ -174,28 +174,7 @@ class ToHtml(Tokenizer):
for tType, tLine, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
if tFormat:
# 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;")
tText = tText.replace("<", "&lt;").replace(">", "&gt;")
# Styles
aStyle = []
@@ -284,8 +263,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:
+65 -42
View File
@@ -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,14 +42,7 @@ from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
ESCAPES = {
r"\*": "*",
r"\~": "~",
r"\_": "_",
r"\[": "[",
r"\]": "]",
r"\ ": "",
}
ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
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
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
##
@@ -399,26 +409,13 @@ 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]),
(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 = []
tmpMarkdown = []
nLine = 0
breakNext = False
for aLine in self._text.splitlines():
nLine += 1
sLine = aLine.strip()
sLine = aLine.strip().lower()
# Check for blank lines
if len(sLine) == 0:
@@ -445,17 +442,17 @@ class Tokenizer(ABC):
# 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, nLine, "", 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(
@@ -586,23 +583,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 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))
# Process formats
tLine, fmtPos = self._extractFormats(aLine)
self._tokens.append((
self.T_TEXT, nLine, aLine, fmtPos, sAlign
self.T_TEXT, nLine, tLine, fmtPos, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -779,6 +763,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
+2 -2
View File
@@ -162,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: