From c08033d153be38cd27251f0bc9fabca0c10ebfe0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:34:04 +0200 Subject: [PATCH] Use stdlib re for dialogue matching --- novelwriter/core/tokenizer.py | 12 ++++----- novelwriter/gui/dochighlight.py | 34 ++++++++------------------ novelwriter/text/patterns.py | 35 +++++++++------------------ tests/test_text/test_text_patterns.py | 23 +++++------------- 4 files changed, 33 insertions(+), 71 deletions(-) diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index ef207074..c98d25e0 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -33,7 +33,7 @@ from functools import partial from pathlib import Path from time import time -from PyQt5.QtCore import QCoreApplication, QRegularExpression +from PyQt5.QtCore import QCoreApplication from PyQt5.QtGui import QFont from novelwriter import CONFIG @@ -234,7 +234,7 @@ class Tokenizer(ABC): nwShortcode.FOOTNOTE_B: self.FMT_FNOTE, } - self._rxDialogue: list[tuple[QRegularExpression, int, int]] = [] + self._rxDialogue: list[tuple[re.Pattern, int, int]] = [] return @@ -1136,11 +1136,9 @@ class Tokenizer(ABC): # Match Dialogue if self._rxDialogue and hDialog: for regEx, fmtB, fmtE in self._rxDialogue: - rxItt = regEx.globalMatch(text, 0) - while rxItt.hasNext(): - rxMatch = rxItt.next() - temp.append((rxMatch.capturedStart(0), 0, fmtB, "")) - temp.append((rxMatch.capturedEnd(0), 0, fmtE, "")) + for match in re.finditer(regEx, text): + temp.append((match.start(0), 0, fmtB, "")) + temp.append((match.end(0), 0, fmtE, "")) # Post-process text and format result = text diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 5cf14e9a..6eb5a5cd 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -29,7 +29,7 @@ import re from time import time -from PyQt5.QtCore import QRegularExpression, Qt +from PyQt5.QtCore import Qt from PyQt5.QtGui import ( QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData, QTextCharFormat, QTextDocument @@ -398,29 +398,15 @@ class GuiDocHighlighter(QSyntaxHighlighter): if hRules: for rX, hRule in hRules: - if isinstance(rX, QRegularExpression): - rxItt = rX.globalMatch(text, xOff) - while rxItt.hasNext(): - rxMatch = rxItt.next() - for xM, hFmt in hRule.items(): - xPos = rxMatch.capturedStart(xM) - xEnd = rxMatch.capturedEnd(xM) - for x in range(xPos, xEnd): - cFmt = self.format(x) - if cFmt.fontStyleName() != "markup": - cFmt.merge(hFmt) - self.setFormat(x, 1, cFmt) - else: - for match in re.finditer(rX, text[xOff:]): - for xM, hFmt in hRule.items(): - # print(f"'{match.group(xM)}'", match.start(xM), match.end(xM)) - xPos = match.start(xM) + xOff - xEnd = match.end(xM) + xOff - for x in range(xPos, xEnd): - cFmt = self.format(x) - if cFmt.fontStyleName() != "markup": - cFmt.merge(hFmt) - self.setFormat(x, 1, cFmt) + for match in re.finditer(rX, text[xOff:]): + for xM, hFmt in hRule.items(): + xPos = match.start(xM) + xOff + xEnd = match.end(xM) + xOff + for x in range(xPos, xEnd): + cFmt = self.format(x) + if cFmt.fontStyleName() != "markup": + cFmt.merge(hFmt) + self.setFormat(x, 1, cFmt) data = self.currentBlockUserData() if not isinstance(data, TextBlockData): diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py index 2e74951b..59a2eaf1 100644 --- a/novelwriter/text/patterns.py +++ b/novelwriter/text/patterns.py @@ -25,11 +25,8 @@ from __future__ import annotations import re -from PyQt5.QtCore import QRegularExpression - from novelwriter import CONFIG from novelwriter.constants import nwRegEx -from novelwriter.types import QRegExUnicode class RegExPatterns: @@ -67,7 +64,7 @@ class RegExPatterns: return self._rxSCValue @property - def dialogStyle(self) -> QRegularExpression: + def dialogStyle(self) -> re.Pattern: """Dialogue detection rule based on user settings.""" symO = "" symC = "" @@ -79,34 +76,26 @@ class RegExPatterns: symC += CONFIG.fmtDQuoteClose rxEnd = "|$" if CONFIG.allowOpenDial else "" - rxRule = QRegularExpression(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})") - rxRule.setPatternOptions(QRegExUnicode) - return rxRule + return re.compile(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})", re.UNICODE) @property - def dialogLine(self) -> QRegularExpression: + def dialogLine(self) -> re.Pattern: """Dialogue line rule based on user settings.""" - sym = QRegularExpression.escape(CONFIG.dialogLine) - rxRule = QRegularExpression(f"^{sym}.*?$") - rxRule.setPatternOptions(QRegExUnicode) - return rxRule + sym = re.escape(CONFIG.dialogLine) + return re.compile(f"^{sym}.*?$", re.UNICODE) @property - def narratorBreak(self) -> QRegularExpression: + def narratorBreak(self) -> re.Pattern: """Dialogue narrator break rule based on user settings.""" - sym = QRegularExpression.escape(CONFIG.narratorBreak) - rxRule = QRegularExpression(f"\\B{sym}\\S.*?\\S{sym}\\B") - rxRule.setPatternOptions(QRegExUnicode) - return rxRule + sym = re.escape(CONFIG.narratorBreak) + return re.compile(f"\\B{sym}\\S.*?\\S{sym}\\B", re.UNICODE) @property - def altDialogStyle(self) -> QRegularExpression: + def altDialogStyle(self) -> re.Pattern: """Dialogue alternative rule based on user settings.""" - symO = QRegularExpression.escape(CONFIG.altDialogOpen) - symC = QRegularExpression.escape(CONFIG.altDialogClose) - rxRule = QRegularExpression(f"\\B{symO}.*?{symC}\\B") - rxRule.setPatternOptions(QRegExUnicode) - return rxRule + symO = re.escape(CONFIG.altDialogOpen) + symC = re.escape(CONFIG.altDialogClose) + return re.compile(f"\\B{symO}.*?{symC}\\B", re.UNICODE) REGEX_PATTERNS = RegExPatterns() diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index b421db52..48f1e4c8 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -24,30 +24,19 @@ import re import pytest -from PyQt5.QtCore import QRegularExpression - from novelwriter import CONFIG from novelwriter.constants import nwUnicode from novelwriter.text.patterns import REGEX_PATTERNS -def allMatches(regEx: QRegularExpression, text: str) -> list[list[str]]: +def allMatches(regEx: re.Pattern, text: str) -> list[list[str]]: """Get all matches for a regex.""" result = [] - if isinstance(regEx, QRegularExpression): - itt = regEx.globalMatch(text, 0) - while itt.hasNext(): - match = itt.next() - result.append([ - (match.captured(n), match.capturedStart(n), match.capturedEnd(n)) - for n in range(match.lastCapturedIndex() + 1) - ]) - else: - for match in re.finditer(regEx, text): - result.append([ - (match.group(n), match.start(n), match.end(n)) - for n in range((match.lastindex or -1) + 1) - ]) + for match in re.finditer(regEx, text): + result.append([ + (match.group(n), match.start(n), match.end(n)) + for n in range((match.lastindex or 0) + 1) + ]) return result