Files
tim 6c2574cf0f Store chapters, merge chapter lists, diff and apply rules
The SQLite store keeps every chapter revision, so a chapter that the site
removes stays in the book. The merge puts new chapters after the chapter
that comes before them on the site. Diffs compare text, not HTML, and
replacement rules run on chapter HTML when a book is built.
2026-09-27 21:23:20 +02:00

91 lines
3.5 KiB
Python

import pytest
from bindery import rules as rules_module
from bindery.rules import RuleError, apply_rules, breaks_to_paragraphs, compile_rules, normalize_rules
def test_rules_apply_in_order_and_skip_disabled():
rules = normalize_rules(
[
{"pattern": "cat", "replacement": "dog"},
{"pattern": "dog", "replacement": "wolf", "enabled": False},
{"pattern": r"(\w+) house", "replacement": r"\1 home", "flags": "I"},
]
)
assert apply_rules("The cat is in the HOUSE", compile_rules(rules)) == "The dog is in the home"
assert apply_rules("a cat House", compile_rules(rules)) == "a dog home"
def test_global_rules_run_before_story_rules():
global_rules = normalize_rules([{"pattern": "a", "replacement": "b"}])
story_rules = normalize_rules([{"pattern": "b", "replacement": "c"}])
assert apply_rules("a", compile_rules(global_rules, story_rules)) == "c"
def test_flags_are_cleaned():
assert normalize_rules([{"pattern": "x", "flags": "smi"}])[0]["flags"] == "ims"
@pytest.mark.parametrize(
"rule, message",
[
({"pattern": ""}, "empty"),
({"pattern": "(open"}, "missing"),
({"pattern": "a", "replacement": r"\3"}, "group"),
({"pattern": "a", "flags": "q"}, "flag"),
],
)
def test_bad_rules_name_the_rule(rule, message):
with pytest.raises(RuleError) as info:
normalize_rules([{"pattern": "fine"}, rule])
assert info.value.index == 1
assert message in str(info.value)
@pytest.mark.parametrize(
"rule",
[
{"pattern": r"(a)(?P<b>b)", "replacement": r"\2 \g<b> \1"},
{"pattern": r"(?i)(a)", "replacement": r"\1"},
{"pattern": "(a) # a comment", "replacement": r"\1", "flags": "x"},
{"pattern": "a", "replacement": r"x\\y\n"},
],
)
def test_good_replacements_pass(rule):
assert normalize_rules([rule])[0]["replacement"] == rule["replacement"]
@pytest.mark.parametrize("replacement", [r"\g<nope>", "\\", r"\q"])
def test_bad_replacements_fail_before_a_match(replacement):
with pytest.raises(RuleError):
normalize_rules([{"pattern": "(a)", "replacement": replacement}])
def test_a_slow_rule_stops_at_the_timeout(monkeypatch):
monkeypatch.setattr(rules_module, "TIMEOUT", 0.1)
steps = compile_rules(normalize_rules([{"pattern": "(a|aa)+$"}]))
with pytest.raises(ValueError, match=r"takes longer than 0.1 seconds"):
apply_rules("a" * 60 + "b", steps)
@pytest.mark.parametrize(
("html", "expected"),
[
("<div>One<br>Two<br/><br/>Three</div>", "<div><p>One</p><p>Two</p><p>Three</p></div>"),
('<div><p class="x" id="p1">A<br>B</p></div>', '<div><p class="x" id="p1">A</p><p class="x">B</p></div>'),
("<div>Loose<br><p>Block</p>After</div>", "<div><p>Loose</p><p>Block</p><p>After</p></div>"),
("<div><p><em>a<br/>b</em></p></div>", "<div><p><em>a<br/>b</em></p></div>"),
('<div>Text<br>&nbsp;<br><img src="i.png"/><br> </div>', '<div><p>Text</p><p><img src="i.png"/></p></div>'),
("<div><p>No breaks</p></div>", "<div><p>No breaks</p></div>"),
],
)
def test_breaks_become_paragraphs(html, expected):
assert breaks_to_paragraphs(html) == expected
def test_paragraphs_come_before_the_rules():
rules = normalize_rules([{"pattern": "<p>", "replacement": '<p class="x">'}])
steps = compile_rules(rules, paragraphs=True)
assert apply_rules("<div>a<br>b</div>", steps) == '<div><p class="x">a</p><p class="x">b</p></div>'
assert apply_rules("<div>a<br>b</div>", compile_rules(rules)) == "<div>a<br>b</div>"