Refactor quoted dialogue detection (#2138)

This commit is contained in:
Veronica Berglyd Olsen
2024-12-23 14:23:53 +01:00
committed by GitHub
2 changed files with 95 additions and 7 deletions
+17 -5
View File
@@ -28,11 +28,13 @@ import re
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import compact, uniqueCompact from novelwriter.common import compact, uniqueCompact
from novelwriter.constants import nwRegEx from novelwriter.constants import nwRegEx, nwUnicode
class RegExPatterns: class RegExPatterns:
AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO)
# Static RegExes # Static RegExes
_rxUrl = re.compile(nwRegEx.URL, re.ASCII) _rxUrl = re.compile(nwRegEx.URL, re.ASCII)
_rxWords = re.compile(nwRegEx.WORDS, re.UNICODE) _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE)
@@ -87,16 +89,25 @@ class RegExPatterns:
def dialogStyle(self) -> re.Pattern | None: def dialogStyle(self) -> re.Pattern | None:
"""Dialogue detection rule based on user settings.""" """Dialogue detection rule based on user settings."""
if CONFIG.dialogStyle > 0: if CONFIG.dialogStyle > 0:
end = "|$" if CONFIG.allowOpenDial else ""
rx = [] rx = []
if CONFIG.dialogStyle in (1, 3): if CONFIG.dialogStyle in (1, 3):
qO = CONFIG.fmtSQuoteOpen.strip()[:1] qO = CONFIG.fmtSQuoteOpen.strip()[:1]
qC = CONFIG.fmtSQuoteClose.strip()[:1] qC = CONFIG.fmtSQuoteClose.strip()[:1]
rx.append(f"(?:\\B{qO}.*?(?:{qC}\\B{end}))") if qO == qC:
rx.append(f"(?:\\B{qO}.+?{qC}\\B)")
else:
rx.append(f"(?:{qO}[^{qO}]+{qC})")
if CONFIG.allowOpenDial:
rx.append(f"(?:{qO}.+?$)")
if CONFIG.dialogStyle in (2, 3): if CONFIG.dialogStyle in (2, 3):
qO = CONFIG.fmtDQuoteOpen.strip()[:1] qO = CONFIG.fmtDQuoteOpen.strip()[:1]
qC = CONFIG.fmtDQuoteClose.strip()[:1] qC = CONFIG.fmtDQuoteClose.strip()[:1]
rx.append(f"(?:\\B{qO}.*?(?:{qC}\\B{end}))") if qO == qC:
rx.append(f"(?:\\B{qO}.+?{qC}\\B)")
else:
rx.append(f"(?:{qO}[^{qO}]+{qC})")
if CONFIG.allowOpenDial:
rx.append(f"(?:{qO}.+?$)")
return re.compile("|".join(rx), re.UNICODE) return re.compile("|".join(rx), re.UNICODE)
return None return None
@@ -106,7 +117,8 @@ class RegExPatterns:
if CONFIG.altDialogOpen and CONFIG.altDialogClose: if CONFIG.altDialogOpen and CONFIG.altDialogClose:
qO = re.escape(compact(CONFIG.altDialogOpen)) qO = re.escape(compact(CONFIG.altDialogOpen))
qC = re.escape(compact(CONFIG.altDialogClose)) qC = re.escape(compact(CONFIG.altDialogClose))
return re.compile(f"\\B{qO}.*?{qC}\\B", re.UNICODE) qB = r"\B" if (qO == qC or qC in self.AMBIGUOUS) else ""
return re.compile(f"{qO}.*?{qC}{qB}", re.UNICODE)
return None return None
+78 -2
View File
@@ -312,8 +312,27 @@ def testTextPatterns_DialogueStyle():
# Straight double quotes are ignored # Straight double quotes are ignored
assert allMatches(regEx, "one \"two\" three") == [] assert allMatches(regEx, "one \"two\" three") == []
# Skipping whitespace is not allowed # Check with no whitespace, single quote
assert allMatches(regEx, "one\u2018two\u2019three") == [] assert allMatches(regEx, "one\u2018two\u2019three") == [
[("\u2018two\u2019", 3, 8)]
]
assert allMatches(regEx, "one\u2018two\u2019 three") == [
[("\u2018two\u2019", 3, 8)]
]
# Check with no whitespace, double quote
assert allMatches(regEx, "one\u201ctwo\u201dthree") == [
[("\u201ctwo\u201d", 3, 8)]
]
assert allMatches(regEx, "one\u201ctwo\u201d three") == [
[("\u201ctwo\u201d", 3, 8)]
]
# Check with apostrophe
assert allMatches(regEx, "one \u2018two\u2019s three\u2019, \u2018four\u2019 five") == [
[("\u2018two\u2019s three\u2019", 4, 17)],
[("\u2018four\u2019", 19, 25)],
]
# Open # Open
# ==== # ====
@@ -333,6 +352,63 @@ def testTextPatterns_DialogueStyle():
] ]
@pytest.mark.core
def testTextPatterns_DialoguePlain():
"""Test the dialogue style pattern regexes for plain quotes."""
# Set the config
CONFIG.fmtSQuoteOpen = "'"
CONFIG.fmtSQuoteClose = "'"
CONFIG.fmtDQuoteOpen = '"'
CONFIG.fmtDQuoteClose = '"'
CONFIG.dialogStyle = 3
CONFIG.allowOpenDial = False
regEx = REGEX_PATTERNS.dialogStyle
assert regEx is not None
# Double
# ======
# One double quoted string
assert allMatches(regEx, "one \"two\" three") == [
[("\"two\"", 4, 9)]
]
# Two double quoted strings
assert allMatches(regEx, "one \"two\" three \"four\" five") == [
[("\"two\"", 4, 9)], [("\"four\"", 16, 22)],
]
# No space
assert allMatches(regEx, "one\"two\" three") == []
assert allMatches(regEx, "one \"two\"three") == []
assert allMatches(regEx, "one\"two\"three") == []
# Single
# ======
# One single quoted string
assert allMatches(regEx, "one 'two' three") == [
[("'two'", 4, 9)]
]
# Two single quoted strings
assert allMatches(regEx, "one 'two' three 'four' five") == [
[("'two'", 4, 9)], [("'four'", 16, 22)],
]
# No space
assert allMatches(regEx, "one'two' three") == []
assert allMatches(regEx, "one 'two'three") == []
assert allMatches(regEx, "one'two'three") == []
# Check with apostrophe
assert allMatches(regEx, "one 'two's three', 'four' five") == [
[("'two's three'", 4, 17)],
[("'four'", 19, 25)],
]
@pytest.mark.core @pytest.mark.core
def testTextPatterns_DialogueSpecial(): def testTextPatterns_DialogueSpecial():
"""Test the special dialogue style pattern regexes.""" """Test the special dialogue style pattern regexes."""