"""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