Add shortcodes functionality
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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("<", "<").replace(">", ">")
|
||||||
# If we have formatting, we must recompute the locations
|
|
||||||
cText = []
|
|
||||||
i = 0
|
|
||||||
for c in tText:
|
|
||||||
if c == "<":
|
|
||||||
cText.append("<")
|
|
||||||
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat]
|
|
||||||
i += 4
|
|
||||||
elif c == ">":
|
|
||||||
cText.append(">")
|
|
||||||
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("<", "<").replace(">", ">")
|
|
||||||
|
|
||||||
# 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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user