Files
Veronica Berglyd Olsen 5bdbd7c195 Update linting for tests
2025-08-27 21:04:32 +02:00

607 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter Patterns Module Tester
====================================
This file is a part of novelWriter
Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" # noqa
from __future__ import annotations
import re
import pytest
from novelwriter import CONFIG
from novelwriter.constants import nwUnicode
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
def allMatches(regEx: re.Pattern, text: str) -> list[list[tuple[str, int, int]]]:
"""Get all matches for a regex."""
return [
[(res.group(n), res.start(n), res.end(n)) for n in range((res.lastindex or 0) + 1)]
for res in regEx.finditer(text)
]
@pytest.mark.core
def testTextPatterns_Urls():
"""Test the URL regex."""
regEx = REGEX_PATTERNS.url
valid = [
"http://example.com",
"http://example.com/",
"http://example.com/path+to+page",
"http://example.com/path-to-page",
"http://example.com/path_to_page",
"http://example.com/path~to~page",
"http://example.com/path/to/page",
"http://example.com/path/to/page.html",
"http://example.com/path/to/page.html#title",
"http://example.com/path/to/page.html#title%20here",
"http://example.com/path/to/page.html#title%20here",
"http://example.com/path/to/page?foo=bar&bar=baz",
"http://example.com/path/to/page.html?foo=bar&bar=baz",
"http://example.com/path/to/page.html#title?foo=bar&bar=baz",
"http://user:password@example.com/",
"http://www.example.com/",
"http://www.www.example.com/",
"http://www.www.www.example.com/",
"https://example.com",
"https://www.example.com/",
]
invalid = [
"hppt://example.com/",
"sftp://example.com/",
"http:/example.com/",
"http://www example com/",
"http://www\texample\tcom/",
]
for test in valid:
assert allMatches(regEx, f"Text {test} more text") == [[(test, 5, 5 + len(test))]]
for test in invalid:
assert allMatches(regEx, f"Text {test} more text") == []
@pytest.mark.core
def testTextPatterns_Words():
"""Test the word split regex."""
regEx = REGEX_PATTERNS.wordSplit
# Spaces
assert allMatches(regEx, "one two three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Hyphens
assert allMatches(regEx, "one-two-three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Em Dashes
assert allMatches(regEx, "one\u2014two\u2014three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Em Dashes
assert allMatches(regEx, "one\u2014two\u2014three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Plus
assert allMatches(regEx, "one+two+three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Slash
assert allMatches(regEx, "one/two/three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Brackets
assert allMatches(regEx, "one[two]three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
# Colon
assert allMatches(regEx, "one:two:three") == [
[("one", 0, 3)], [("two", 4, 7)], [("three", 8, 13)]
]
@pytest.mark.core
def testTextPatterns_Markdown():
"""Test the markdown pattern regexes."""
# Bold
regEx = REGEX_PATTERNS.markdownBold
assert allMatches(regEx, "one **two** three") == [
[("**two**", 4, 11), ("**", 4, 6), ("two", 6, 9), ("**", 9, 11)]
]
assert allMatches(regEx, "one **two* three") == []
assert allMatches(regEx, "one *two** three") == []
assert allMatches(regEx, "one**two**three") == []
# Italic
regEx = REGEX_PATTERNS.markdownItalic
assert allMatches(regEx, "one _two_ three") == [
[("_two_", 4, 9), ("_", 4, 5), ("two", 5, 8), ("_", 8, 9)]
]
assert allMatches(regEx, "one __two_ three") == []
assert allMatches(regEx, "one _two__ three") == [
[("_two__", 4, 10), ("_", 4, 5), ("two_", 5, 9), ("_", 9, 10)]
]
assert allMatches(regEx, "one_two_three") == []
# Strike
regEx = REGEX_PATTERNS.markdownStrike
assert allMatches(regEx, "one ~~two~~ three") == [
[("~~two~~", 4, 11), ("~~", 4, 6), ("two", 6, 9), ("~~", 9, 11)]
]
assert allMatches(regEx, "one ~~two~ three") == []
assert allMatches(regEx, "one ~two~~ three") == []
assert allMatches(regEx, "one~~two~~three") == []
# Mark
regEx = REGEX_PATTERNS.markdownMark
assert allMatches(regEx, "one ==two== three") == [
[("==two==", 4, 11), ("==", 4, 6), ("two", 6, 9), ("==", 9, 11)]
]
assert allMatches(regEx, "one ==two= three") == []
assert allMatches(regEx, "one =two== three") == []
assert allMatches(regEx, "one==two==three") == []
@pytest.mark.core
def testTextPatterns_ShortcodesPlain():
"""Test the shortcode pattern regexes."""
regEx = REGEX_PATTERNS.shortcodePlain
# Test Usage
# ==========
# General, normal usage
assert allMatches(regEx, "one [b]two[/b] three") == [
[("[b]", 4, 7), ("[b]", 4, 7)],
[("[/b]", 10, 14), ("[/b]", 10, 14)],
]
# General, no spaces
assert allMatches(regEx, "one[b]two[/b]three") == [
[("[b]", 3, 6), ("[b]", 3, 6)],
[("[/b]", 9, 13), ("[/b]", 9, 13)],
]
# General, with padding
assert allMatches(regEx, "one [b] two [/b] three") == [
[("[b]", 4, 7), ("[b]", 4, 7)],
[("[/b]", 12, 16), ("[/b]", 12, 16)],
]
# General, with escapes
assert allMatches(regEx, "one \\[b]two[/b\\] three") == []
# Test Formats
# ============
# Bold
assert allMatches(regEx, "one [b]two[/b] three") == [
[("[b]", 4, 7), ("[b]", 4, 7)],
[("[/b]", 10, 14), ("[/b]", 10, 14)],
]
# Italic
assert allMatches(regEx, "one [i]two[/i] three") == [
[("[i]", 4, 7), ("[i]", 4, 7)],
[("[/i]", 10, 14), ("[/i]", 10, 14)],
]
# Strike
assert allMatches(regEx, "one [s]two[/s] three") == [
[("[s]", 4, 7), ("[s]", 4, 7)],
[("[/s]", 10, 14), ("[/s]", 10, 14)],
]
# Underline
assert allMatches(regEx, "one [u]two[/u] three") == [
[("[u]", 4, 7), ("[u]", 4, 7)],
[("[/u]", 10, 14), ("[/u]", 10, 14)],
]
# Mark
assert allMatches(regEx, "one [m]two[/m] three") == [
[("[m]", 4, 7), ("[m]", 4, 7)],
[("[/m]", 10, 14), ("[/m]", 10, 14)],
]
# Superscript
assert allMatches(regEx, "one [sup]two[/sup] three") == [
[("[sup]", 4, 9), ("[sup]", 4, 9)],
[("[/sup]", 12, 18), ("[/sup]", 12, 18)],
]
# Subscript
assert allMatches(regEx, "one [sub]two[/sub] three") == [
[("[sub]", 4, 9), ("[sub]", 4, 9)],
[("[/sub]", 12, 18), ("[/sub]", 12, 18)],
]
# Test Invalid
# ============
assert allMatches(regEx, "one [x]two[/x] three") == []
@pytest.mark.core
def testTextPatterns_LineBreakReplace():
"""Test replacing forced line breaks."""
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
def testTextPatterns_ShortcodesValue():
"""Test the shortcode with value pattern regexes."""
regEx = REGEX_PATTERNS.shortcodeValue
assert allMatches(regEx, "one [footnote:two] three") == [
[("[footnote:two]", 4, 18), ("[footnote:", 4, 14), ("two", 14, 17), ("]", 17, 18)]
]
@pytest.mark.core
def testTextPatterns_DialogueStyle():
"""Test the dialogue style pattern regexes."""
# Before set, the regex is None
CONFIG.dialogStyle = 0
assert REGEX_PATTERNS.dialogStyle is None
# Set the config
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
CONFIG.dialogStyle = 3
# Closed
# ======
CONFIG.allowOpenDial = False
regEx = REGEX_PATTERNS.dialogStyle
assert regEx is not None
# Defined single quotes are recognised
assert allMatches(regEx, "one \u2018two\u2019 three") == [
[("\u2018two\u2019", 4, 9)]
]
# Defined double quotes are recognised
assert allMatches(regEx, "one \u201ctwo\u201d three") == [
[("\u201ctwo\u201d", 4, 9)]
]
# Both single and double quotes are recognised
assert allMatches(regEx, "one \u2018two\u2019 three \u201cfour\u201d five") == [
[("\u2018two\u2019", 4, 9)], [("\u201cfour\u201d", 16, 22)]
]
# But not mixed
assert allMatches(regEx, "one \u2018two\u201d three \u201cfour\u2019 five") == [
[("\u2018two\u201d three \u201cfour\u2019", 4, 22)]
]
# Straight single quotes are ignored
assert allMatches(regEx, "one 'two' three") == []
# Straight double quotes are ignored
assert allMatches(regEx, 'one "two" three') == []
# Check with no whitespace, single quote
# The apostrophe ambiguity rule should kick in here
assert allMatches(regEx, "one\u2018two\u2019three") == []
assert allMatches(regEx, "one\u2018two\u2019 three") == []
assert allMatches(regEx, "one \u2018two\u02bc three\u2019 four") == [
[("\u2018two\u02bc three\u2019", 4, 16)],
]
# 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
# ====
CONFIG.allowOpenDial = True
regEx = REGEX_PATTERNS.dialogStyle
assert regEx is not None
# Defined single quotes are recognised also when open
assert allMatches(regEx, "one \u2018two three") == [
[("\u2018two three", 4, 14)]
]
# Defined double quotes are recognised also when open
assert allMatches(regEx, "one \u201ctwo three") == [
[("\u201ctwo three", 4, 14)]
]
@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
def testTextPatterns_DialogueSpecial():
"""Test the special dialogue style pattern regexes."""
# Before set, the regex is None
CONFIG.altDialogOpen = ""
CONFIG.altDialogClose = ""
assert REGEX_PATTERNS.altDialogStyle is None
# Set the config
CONFIG.altDialogOpen = "::"
CONFIG.altDialogClose = "::"
# Alternative Dialogue
# ====================
regEx = REGEX_PATTERNS.altDialogStyle
assert regEx is not None
# With no padding
assert allMatches(regEx, "one ::two:: three") == [
[("::two::", 4, 11)]
]
# With padding
assert allMatches(regEx, "one :: two :: three") == [
[(":: two ::", 4, 13)]
]
@pytest.mark.core
def testTextPatterns_DialogParserEnglish():
"""Test the dialog parser with English settings."""
# Set the config
CONFIG.dialogStyle = 3
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
parser = DialogParser()
parser.initParser()
assert parser.enabled is True
# Positions: 0 18
assert parser("“Simple dialogue.”") == [
(0, 18),
]
# Positions: 0 18
assert parser("“Simple dialogue,” argued John.") == [
(0, 18),
]
# Positions: 0 18 32 56
assert parser("“Simple dialogue,” argued John, “is not always so easy.”") == [
(0, 18), (32, 56),
]
# With Narrator breaks
CONFIG.dialogLine = ""
CONFIG.narratorBreak = nwUnicode.U_EMDASH
parser.initParser()
# Positions: 0 18 34 58
assert parser("“Simple dialogue, — argued John, — is not always so easy.”") == [
(0, 18), (34, 58),
]
# Positions: 0 18 32 56
assert parser("“Simple dialogue, —argued John—, is not always so easy.”") == [
(0, 18), (32, 56),
]
# Positions: 0 31
assert parser("“Simple dialogue, —argued John”") == [
(0, 31),
]
@pytest.mark.core
def testTextPatterns_DialogParserSpanish():
"""Test the dialog parser with Spanish settings."""
# Set the config
CONFIG.dialogStyle = 3
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSAQUO
CONFIG.fmtSQuoteClose = nwUnicode.U_RSAQUO
CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO
CONFIG.dialogLine = nwUnicode.U_EMDASH + nwUnicode.U_RAQUO
CONFIG.narratorBreak = nwUnicode.U_EMDASH
parser = DialogParser()
parser.initParser()
# Positions: 0 18 54 70
assert parser("—No te preocupes. —Cerró la puerta y salió corriendo—. Volveré pronto.") == [
(0, 18), (54, 70),
]
# Positions: 0 14
assert parser("«Tengo hambre», pensó Pedro.") == [
(0, 14),
]
# Positions: 0 16
assert parser("—Puedes hacerlo —le dije y pensé «pero te costará mucho trabajo».") == [
(0, 16),
]
@pytest.mark.core
def testTextPatterns_DialogParserPortuguese():
"""Test the dialog parser with Portuguese settings."""
# Set the config
CONFIG.dialogStyle = 0
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSAQUO
CONFIG.fmtSQuoteClose = nwUnicode.U_RSAQUO
CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO
CONFIG.dialogLine = nwUnicode.U_EMDASH
CONFIG.narratorBreak = nwUnicode.U_EMDASH
parser = DialogParser()
parser.initParser()
# Positions: 0 21
assert parser("— Está ficando tarde.") == [
(0, 21),
]
# Positions: 0 12
assert parser("— Ainda não — ela responde.") == [
(0, 12),
]
# Positions: 0 12 29 49
assert parser("— Tudo bem? — ele pergunta. — Você falou com ele?") == [
(0, 12), (29, 49),
]
# Positions: 0 12 29 49
assert parser("— Tudo bem? — ele pergunta —. Você falou com ele?") == [
(0, 12), (29, 49),
]
@pytest.mark.core
def testTextPatterns_DialogParserPolish():
"""Test the dialog parser with alternating Polish settings."""
# Set the config
CONFIG.dialogStyle = 0
CONFIG.fmtSQuoteOpen = "'"
CONFIG.fmtSQuoteClose = "'"
CONFIG.fmtDQuoteOpen = '"'
CONFIG.fmtDQuoteClose = '"'
CONFIG.dialogLine = ""
CONFIG.narratorBreak = ""
CONFIG.narratorDialog = nwUnicode.U_ENDASH
parser = DialogParser()
parser.initParser()
# This is what an example dialogue might look like using Polish punctuation rules
# See discussion #1976
assert parser(
" Example statement someone said. And he added: Another example statement."
) == [
(0, 20), (50, 78),
]
assert parser(
" Oh my! It would be nice if only the statements were highlighted, without "
"any narration. In a paragraph where there is only a short statement and then "
"a lot happens, this would be especially justified."
) == [
(0, 9),
]
assert parser(
"There are also sometimes paragraphs that start with a narrative, and only then "
"someone shouts out the words: Oooh! Look!"
) == [
(109, 122),
]
assert parser(
'And so on and so forth. However, "text in quotation marks" should not be '
"highlighted at all, and if so, it should be highlighted differently."
) == []