Refactor quoted dialogue detection (#2138)
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user