Add line break shortcode support

This commit is contained in:
Veronica Berglyd Olsen
2024-10-24 01:07:03 +02:00
parent 560540618c
commit ff7404d01d
9 changed files with 45 additions and 15 deletions
+3 -1
View File
@@ -61,10 +61,11 @@ class nwConst:
class nwRegEx: class nwRegEx:
WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b" WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
BREAK = r"(?i)(?<!\\)(\[br\]\n?)"
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)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])" FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])" FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
@@ -84,6 +85,7 @@ class nwShortcode:
SUP_C = "[/sup]" SUP_C = "[/sup]"
SUB_O = "[sub]" SUB_O = "[sub]"
SUB_C = "[/sub]" SUB_C = "[/sub]"
BREAK = "[br]"
FOOTNOTE_B = "[footnote:" FOOTNOTE_B = "[footnote:"
+1
View File
@@ -137,6 +137,7 @@ class nwDocInsert(Enum):
VSPACE_M = 9 VSPACE_M = 9
LIPSUM = 10 LIPSUM = 10
FOOTNOTE = 11 FOOTNOTE = 11
LINE_BRK = 12
class nwView(Enum): class nwView(Enum):
+4 -8
View File
@@ -56,7 +56,6 @@ class ComStyle(NamedTuple):
textClass: str = "" textClass: str = ""
B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
COMMENT_STYLE = { COMMENT_STYLE = {
nwComment.PLAIN: ComStyle("Comment", "comment", "comment"), nwComment.PLAIN: ComStyle("Comment", "comment", "comment"),
nwComment.IGNORE: ComStyle(), nwComment.IGNORE: ComStyle(),
@@ -67,13 +66,12 @@ COMMENT_STYLE = {
nwComment.COMMENT: ComStyle(), nwComment.COMMENT: ComStyle(),
nwComment.STORY: ComStyle("", "modifier", "note"), nwComment.STORY: ComStyle("", "modifier", "note"),
} }
# Lookups
HEADINGS = [BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4] HEADINGS = [BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4]
SKIP_INDENT = [ SKIP_INDENT = [
BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD2, BlockTyp.HEAD3,
BlockTyp.HEAD4, BlockTyp.SEP, BlockTyp.SKIP, BlockTyp.HEAD4, BlockTyp.SEP, BlockTyp.SKIP,
] ]
B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
class Tokenizer(ABC): class Tokenizer(ABC):
@@ -182,8 +180,6 @@ class Tokenizer(ABC):
(REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]), (REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]),
(REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]), (REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]),
] ]
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
self._shortCodeFmt = { self._shortCodeFmt = {
nwShortcode.ITALIC_O: TextFmt.I_B, nwShortcode.ITALIC_C: TextFmt.I_E, nwShortcode.ITALIC_O: TextFmt.I_B, nwShortcode.ITALIC_C: TextFmt.I_E,
@@ -1136,12 +1132,12 @@ class Tokenizer(ABC):
# Post-process text and format # Post-process text and format
result = text result = text
formats = [] formats = []
for pos, end, fmt, key in reversed(sorted(temp, key=lambda x: x[0])): for pos, end, fmt, meta in reversed(sorted(temp, key=lambda x: x[0])):
if fmt > 0: if fmt > 0:
if end > pos: if end > pos:
result = result[:pos] + result[end:] result = result[:pos] + result[end:]
formats = [(p+pos-end if p > pos else p, f, k) for p, f, k in formats] formats = [(p+pos-end if p > pos else p, f, m) for p, f, m in formats]
formats.insert(0, (pos, fmt, key)) formats.insert(0, (pos, fmt, meta))
return result, formats return result, formats
+2
View File
@@ -877,6 +877,8 @@ class GuiDocEditor(QPlainTextEdit):
after = False after = False
elif insert == nwDocInsert.FOOTNOTE: elif insert == nwDocInsert.FOOTNOTE:
self._insertCommentStructure(nwComment.FOOTNOTE) self._insertCommentStructure(nwComment.FOOTNOTE)
elif insert == nwDocInsert.LINE_BRK:
text = nwShortcode.BREAK
if text: if text:
if block: if block:
+8 -2
View File
@@ -570,8 +570,8 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.SHORT) lambda: self.requestDocInsert.emit(nwDocInsert.SHORT)
) )
# Insert > Symbols # Insert > Breaks and Vertical Space
self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space")) self.mInsBreaks = self.insMenu.addMenu(self.tr("Breaks and Vertical Space"))
# Insert > New Page # Insert > New Page
self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break")) self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break"))
@@ -579,6 +579,12 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.NEW_PAGE) lambda: self.requestDocInsert.emit(nwDocInsert.NEW_PAGE)
) )
# Insert > Forced Line Break
self.aInsLineBreak = self.mInsBreaks.addAction(self.tr("Forced Line Break"))
self.aInsLineBreak.triggered.connect(
lambda: self.requestDocInsert.emit(nwDocInsert.LINE_BRK)
)
# Insert > Vertical Space (Single) # Insert > Vertical Space (Single)
self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)")) self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)"))
self.aInsVSpaceS.triggered.connect( self.aInsVSpaceS.triggered.connect(
+6
View File
@@ -33,6 +33,7 @@ class RegExPatterns:
# Static RegExes # Static RegExes
_rxWords = re.compile(nwRegEx.WORDS, re.UNICODE) _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE)
_rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE)
_rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE) _rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE)
_rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE) _rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE)
_rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE) _rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE)
@@ -44,6 +45,11 @@ class RegExPatterns:
"""Split text into words.""" """Split text into words."""
return self._rxWords return self._rxWords
@property
def lineBreak(self) -> re.Pattern:
"""Find forced line break."""
return self._rxBreak
@property @property
def markdownItalic(self) -> re.Pattern: def markdownItalic(self) -> re.Pattern:
"""Markdown italic style.""" """Markdown italic style."""
+7 -2
View File
@@ -522,13 +522,18 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd
nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger) nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n" assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n"
# Insert Break or Space # Breaks and Vertical Space
# ===================== # =========================
nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger) nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n" assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n"
nwGUI.docEditor.setPlainText("Line OneLine Two\n")
nwGUI.docEditor.setCursorPosition(8)
nwGUI.mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Line One[br]Line Two\n"
nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger) nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n" assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n"
+3 -2
View File
@@ -47,6 +47,7 @@ def testTextCounting_preProcessText():
"[vspace:3]\n\n" "[vspace:3]\n\n"
"[New Page]\n\n" "[New Page]\n\n"
"[footnote:abcd]\n\n" "[footnote:abcd]\n\n"
"[br]\n\n"
"Dashes\u2013and even longer\u2014dashes.\n\n" "Dashes\u2013and even longer\u2014dashes.\n\n"
) )
@@ -60,7 +61,7 @@ def testTextCounting_preProcessText():
"#### Heading Four", "#### Heading Four",
"", "", "", "", "", "",
"A paragraph.", "", "A paragraph.", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"Dashes and even longer dashes.", "" "Dashes and even longer dashes.", ""
] ]
@@ -68,7 +69,7 @@ def testTextCounting_preProcessText():
assert preProcessText(text, keepHeaders=False) == [ assert preProcessText(text, keepHeaders=False) == [
"", "", "", "", "", "",
"A paragraph.", "", "A paragraph.", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"Dashes and even longer dashes.", "" "Dashes and even longer dashes.", ""
] ]
+11
View File
@@ -198,6 +198,17 @@ def testTextPatterns_ShortcodesPlain():
assert allMatches(regEx, "one [x]two[/x] three") == [] assert allMatches(regEx, "one [x]two[/x] three") == []
# Line Break Substitution
# =======================
regEx = REGEX_PATTERNS.lineBreak
assert regEx.sub("\n", "one[br]two") == "one\ntwo"
assert regEx.sub("\n", "one[br]\ntwo") == "one\ntwo"
assert regEx.sub("\n", "one[br]\n\ntwo") == "one\n\ntwo"
assert regEx.sub("\n", "one[BR]two") == "one\ntwo"
assert regEx.sub("\n", "one[BR]\ntwo") == "one\ntwo"
assert regEx.sub("\n", "one[BR]\n\ntwo") == "one\n\ntwo"
@pytest.mark.core @pytest.mark.core
def testTextPatterns_ShortcodesValue(): def testTextPatterns_ShortcodesValue():