"""Tests for furigana parsing and generation utilities."""
from __future__ import annotations
import pytest
from src.utils.furigana import (
FuriganaAnnotation,
extract_all_readings,
format_furigana_html,
format_furigana_inline,
parse_furigana,
validate_furigana_text,
)
class TestParseFurigana:
"""Tests for parse_furigana function."""
def test_parse_simple_furigana(self) -> None:
"""Test parsing simple text with furigana."""
text = "神[かみ]は愛[あい]です"
plain, annotations = parse_furigana(text)
assert plain == "神は愛です"
assert len(annotations) == 2
assert annotations[0].start_offset == 0
assert annotations[0].end_offset == 1
assert annotations[0].base_text == "神"
assert annotations[0].reading == "かみ"
assert annotations[1].start_offset == 2
assert annotations[1].end_offset == 3
assert annotations[1].base_text == "愛"
assert annotations[1].reading == "あい"
def test_parse_no_furigana(self) -> None:
"""Test parsing text without furigana."""
text = "こんにちは"
plain, annotations = parse_furigana(text)
assert plain == "こんにちは"
assert annotations == []
def test_parse_multiple_kanji_compound(self) -> None:
"""Test parsing compound kanji with single reading."""
text = "聖書[せいしょ]を読[よ]む"
plain, annotations = parse_furigana(text)
assert plain == "聖書を読む"
assert len(annotations) == 2
assert annotations[0].base_text == "聖書"
assert annotations[0].reading == "せいしょ"
assert annotations[0].start_offset == 0
assert annotations[0].end_offset == 2
assert annotations[1].base_text == "読"
assert annotations[1].reading == "よ"
assert annotations[1].start_offset == 3
assert annotations[1].end_offset == 4
def test_parse_consecutive_furigana(self) -> None:
"""Test parsing consecutive furigana annotations."""
text = "日本[にほん]語[ご]"
plain, annotations = parse_furigana(text)
assert plain == "日本語"
assert len(annotations) == 2
assert annotations[0].start_offset == 0
assert annotations[0].end_offset == 2
assert annotations[1].start_offset == 2
assert annotations[1].end_offset == 3
def test_parse_katakana_reading(self) -> None:
"""Test parsing with katakana reading."""
text = "神[カミ]"
plain, annotations = parse_furigana(text)
assert plain == "神"
assert len(annotations) == 1
assert annotations[0].reading == "カミ"
def test_parse_empty_string(self) -> None:
"""Test parsing empty string."""
plain, annotations = parse_furigana("")
assert plain == ""
assert annotations == []
def test_annotation_defaults(self) -> None:
"""Test that FuriganaAnnotation has correct defaults."""
_, annotations = parse_furigana("神[かみ]")
assert annotations[0].is_standard_reading is True
assert annotations[0].reading_source == "dictionary"
def test_annotation_frozen(self) -> None:
"""Test that FuriganaAnnotation is immutable."""
annotation = FuriganaAnnotation(0, 1, "神", "かみ")
with pytest.raises(AttributeError):
annotation.reading = "しん" # type: ignore[misc]
class TestFormatFuriganaInline:
"""Tests for format_furigana_inline function."""
def test_format_simple(self) -> None:
"""Test formatting simple annotations."""
annotations = [
FuriganaAnnotation(0, 1, "神", "かみ"),
FuriganaAnnotation(2, 3, "愛", "あい"),
]
result = format_furigana_inline("神は愛です", annotations)
assert result == "神[かみ]は愛[あい]です"
def test_format_no_annotations(self) -> None:
"""Test formatting text without annotations."""
result = format_furigana_inline("こんにちは", [])
assert result == "こんにちは"
def test_format_unsorted_annotations(self) -> None:
"""Test that annotations are sorted by offset."""
annotations = [
FuriganaAnnotation(2, 3, "愛", "あい"), # Out of order
FuriganaAnnotation(0, 1, "神", "かみ"),
]
result = format_furigana_inline("神は愛です", annotations)
assert result == "神[かみ]は愛[あい]です"
def test_format_compound_kanji(self) -> None:
"""Test formatting compound kanji."""
annotations = [
FuriganaAnnotation(0, 2, "聖書", "せいしょ"),
]
result = format_furigana_inline("聖書", annotations)
assert result == "聖書[せいしょ]"
def test_roundtrip(self) -> None:
"""Test parse -> format roundtrip produces same output."""
original = "神[かみ]は愛[あい]です"
plain, annotations = parse_furigana(original)
reconstructed = format_furigana_inline(plain, annotations)
assert reconstructed == original
class TestFormatFuriganaHtml:
"""Tests for format_furigana_html function."""
def test_format_simple_html(self) -> None:
"""Test HTML ruby formatting."""
annotations = [
FuriganaAnnotation(0, 1, "神", "かみ"),
FuriganaAnnotation(2, 3, "愛", "あい"),
]
result = format_furigana_html("神は愛です", annotations)
expected = "神は愛です"
assert result == expected
def test_format_no_annotations_html(self) -> None:
"""Test HTML formatting without annotations."""
result = format_furigana_html("こんにちは", [])
assert result == "こんにちは"
def test_format_unsorted_html(self) -> None:
"""Test that annotations are sorted for HTML output."""
annotations = [
FuriganaAnnotation(2, 3, "愛", "あい"),
FuriganaAnnotation(0, 1, "神", "かみ"),
]
result = format_furigana_html("神は愛です", annotations)
expected = "神は愛です"
assert result == expected
class TestValidateFuriganaText:
"""Tests for validate_furigana_text function."""
def test_valid_text(self) -> None:
"""Test validation of valid furigana text."""
errors = validate_furigana_text("神[かみ]は愛[あい]です")
assert errors == []
def test_unmatched_closing_bracket(self) -> None:
"""Test detection of unmatched closing bracket."""
errors = validate_furigana_text("神かみ]は")
assert len(errors) == 1
assert "Unmatched closing bracket" in errors[0]
def test_unmatched_opening_bracket(self) -> None:
"""Test detection of unmatched opening bracket."""
errors = validate_furigana_text("神[かみは")
assert len(errors) == 1
assert "Unmatched opening bracket" in errors[0]
def test_nested_brackets(self) -> None:
"""Test detection of nested brackets."""
errors = validate_furigana_text("神[[かみ]]")
assert any("Nested bracket" in e for e in errors)
def test_non_kanji_with_furigana(self) -> None:
"""Test detection of non-kanji with furigana."""
errors = validate_furigana_text("あ[い]")
assert len(errors) == 1
assert "Non-kanji character" in errors[0]
def test_empty_text(self) -> None:
"""Test validation of empty text."""
errors = validate_furigana_text("")
assert errors == []
class TestExtractAllReadings:
"""Tests for extract_all_readings function."""
def test_extract_single_reading(self) -> None:
"""Test extracting single reading per kanji."""
readings = extract_all_readings("神[かみ]は愛[あい]です")
assert readings == {"神": ["かみ"], "愛": ["あい"]}
def test_extract_multiple_readings(self) -> None:
"""Test extracting multiple readings for same kanji."""
readings = extract_all_readings("神[かみ]は神[しん]の愛[あい]")
assert "神" in readings
assert "かみ" in readings["神"]
assert "しん" in readings["神"]
assert len(readings["神"]) == 2
assert readings["愛"] == ["あい"]
def test_extract_no_readings(self) -> None:
"""Test extracting from text without furigana."""
readings = extract_all_readings("こんにちは")
assert readings == {}
def test_no_duplicate_readings(self) -> None:
"""Test that duplicate readings are not added."""
readings = extract_all_readings("神[かみ]と神[かみ]")
assert readings == {"神": ["かみ"]}
class TestFuriganaAnnotationDataclass:
"""Tests for FuriganaAnnotation dataclass."""
def test_create_with_all_fields(self) -> None:
"""Test creating annotation with all fields."""
annotation = FuriganaAnnotation(
start_offset=0,
end_offset=1,
base_text="神",
reading="かみ",
is_standard_reading=False,
reading_source="theological",
)
assert annotation.start_offset == 0
assert annotation.end_offset == 1
assert annotation.base_text == "神"
assert annotation.reading == "かみ"
assert annotation.is_standard_reading is False
assert annotation.reading_source == "theological"
def test_create_with_defaults(self) -> None:
"""Test creating annotation with default values."""
annotation = FuriganaAnnotation(0, 1, "神", "かみ")
assert annotation.is_standard_reading is True
assert annotation.reading_source == "dictionary"
def test_annotation_equality(self) -> None:
"""Test annotation equality comparison."""
a1 = FuriganaAnnotation(0, 1, "神", "かみ")
a2 = FuriganaAnnotation(0, 1, "神", "かみ")
a3 = FuriganaAnnotation(0, 1, "神", "しん")
assert a1 == a2
assert a1 != a3
def test_annotation_hash(self) -> None:
"""Test that annotations are hashable (frozen dataclass)."""
annotation = FuriganaAnnotation(0, 1, "神", "かみ")
annotations_set = {annotation}
assert annotation in annotations_set