Trim tests
This commit is contained in:
parent
15eac9b1bf
commit
eca44a15bd
6 changed files with 129 additions and 637 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Tests for WOLF inline control-code repair."""
|
"""Tests for WOLF inline control-code repair and font scaling."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -59,95 +59,33 @@ class WolfCodesRepairTests(unittest.TestCase):
|
||||||
"Fortune-teller\nHa!\\^",
|
"Fortune-teller\nHa!\\^",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_apply_font_size_replaces_existing_codes(self):
|
|
||||||
text = r"Go to the \c[21]\f[20]tavern\c[19]\f[18]!"
|
class WolfFontScaleTests(unittest.TestCase):
|
||||||
new_text, count = wolf_codes.apply_font_size(text, 16)
|
def test_scale_keeps_emphasis_ratio_and_leads_with_body(self):
|
||||||
self.assertEqual(count, 3) # 2 replaced + leading body
|
# Cafe-style mid-line emphasis: body must lead so Wolf sets line height.
|
||||||
self.assertTrue(new_text.startswith(r"\f[16]"))
|
text = (
|
||||||
self.assertIn(r"\f[16]", new_text)
|
'"That \\c[21]\\f[20]cafe\\c[19]\\f[18] over there has been\n'
|
||||||
|
"packed lately.\""
|
||||||
|
)
|
||||||
|
new_text, _ = wolf_codes.scale_font_sizes(text, 13)
|
||||||
|
self.assertTrue(new_text.startswith(r"\f[13]"))
|
||||||
|
# 20 * 13/18 ≈ 14.4 → 14; body 13
|
||||||
|
self.assertIn(r"\c[21]\f[14]cafe\c[19]\f[13]", new_text)
|
||||||
self.assertNotIn(r"\f[20]", new_text)
|
self.assertNotIn(r"\f[20]", new_text)
|
||||||
self.assertNotIn(r"\f[18]", new_text)
|
self.assertNotIn(r"\f[18]", new_text)
|
||||||
|
|
||||||
def test_apply_font_size_prepends_when_missing(self):
|
|
||||||
text = "Plain text without font codes"
|
|
||||||
new_text, count = wolf_codes.apply_font_size(text, 18)
|
|
||||||
self.assertEqual(count, 1)
|
|
||||||
self.assertTrue(new_text.startswith(r"\f[18]"))
|
|
||||||
|
|
||||||
def test_infer_base_font_prefers_body_over_emphasis(self):
|
|
||||||
text = r"So they even take worries\nlike this in the\n\c[21]\f[20]confessional\c[19]\f[18]..."
|
|
||||||
self.assertEqual(wolf_codes.infer_base_font_size(text), 18)
|
self.assertEqual(wolf_codes.infer_base_font_size(text), 18)
|
||||||
|
|
||||||
def test_scale_font_sizes_keeps_emphasis_ratio(self):
|
def test_font_size_codes_differ_only_for_size_drift(self):
|
||||||
text = (
|
|
||||||
r"So they even take worries\nlike this in the\n"
|
|
||||||
r"\c[21]\f[20]confessional\c[19]\f[18]..."
|
|
||||||
)
|
|
||||||
new_text, count = wolf_codes.scale_font_sizes(text, 14)
|
|
||||||
self.assertEqual(count, 3) # 2 scaled + leading body insert
|
|
||||||
# 20 * 14/18 ≈ 15.56 → 16; 18 * 14/18 = 14
|
|
||||||
self.assertTrue(new_text.startswith(r"\f[14]"))
|
|
||||||
self.assertIn(r"\f[16]", new_text)
|
|
||||||
self.assertIn(r"\f[14]", new_text)
|
|
||||||
self.assertNotIn(r"\f[20]", new_text)
|
|
||||||
self.assertNotIn(r"\f[18]", new_text)
|
|
||||||
|
|
||||||
def test_scale_font_sizes_matches_names_wrap_style_shrink(self):
|
|
||||||
source_style = r"\c[21]\f[20]Brothel\c[19]\f[18] doesn't get much use"
|
|
||||||
new_text, _ = wolf_codes.scale_font_sizes(source_style, 17)
|
|
||||||
# 20*17/18 ≈ 18.89 → 19; body 17
|
|
||||||
self.assertTrue(new_text.startswith(r"\f[17]"))
|
|
||||||
self.assertIn(r"\f[19]", new_text)
|
|
||||||
self.assertIn(r"\f[17]", new_text)
|
|
||||||
|
|
||||||
def test_scale_font_sizes_leads_with_body_for_midline_emphasis(self):
|
|
||||||
# Cafe-style: body only appears after emphasis; Wolf needs \\f at start.
|
|
||||||
text = (
|
|
||||||
'"That \\c[21]\\f[14]cafe\\c[19]\\f[13] over there has been\n'
|
|
||||||
'packed lately." "Apparently\n'
|
|
||||||
"they're hiring waitresses.\""
|
|
||||||
)
|
|
||||||
new_text, count = wolf_codes.scale_font_sizes(text, 13)
|
|
||||||
self.assertGreaterEqual(count, 2)
|
|
||||||
self.assertTrue(new_text.startswith(r"\f[13]"))
|
|
||||||
self.assertIn(r"\c[21]\f[14]cafe\c[19]\f[13]", new_text)
|
|
||||||
|
|
||||||
def test_ensure_leading_font_size_idempotent(self):
|
|
||||||
text = r'\f[13]"That \c[21]\f[14]cafe\c[19]\f[13] over there"'
|
|
||||||
self.assertEqual(wolf_codes.ensure_leading_font_size(text, 13), text)
|
|
||||||
|
|
||||||
def test_apply_font_size_also_leads(self):
|
|
||||||
text = r'"That \c[21]\f[20]cafe\c[19]\f[18] over there"'
|
|
||||||
new_text, _ = wolf_codes.apply_font_size(text, 13)
|
|
||||||
self.assertTrue(new_text.startswith(r"\f[13]"))
|
|
||||||
self.assertNotIn(r"\f[20]", new_text)
|
|
||||||
|
|
||||||
def test_font_size_codes_differ_detects_names_wrap_shrink(self):
|
|
||||||
source = (
|
|
||||||
r"「おい!例の\c[21]\f[20]教会の特別奉仕活動\c[19]\f[18]があるってよ!」"
|
|
||||||
)
|
|
||||||
text = (
|
|
||||||
r'\f[14]"Hey! Apparently there\'s one of those '
|
|
||||||
r'\c[21]\f[16]Special Service Activities at the church\c[19]\f[14]!"'
|
|
||||||
)
|
|
||||||
self.assertTrue(wolf_codes.font_size_codes_differ(source, text))
|
|
||||||
|
|
||||||
def test_font_size_codes_differ_false_when_color_missing(self):
|
|
||||||
source = r"\c[21]\f[20]娼館\c[19]\f[18]"
|
source = r"\c[21]\f[20]娼館\c[19]\f[18]"
|
||||||
text = r"\f[14]Brothel" # dropped colour codes
|
shrunk = r"\f[14]\c[21]\f[16]Brothel\c[19]\f[14]"
|
||||||
self.assertFalse(wolf_codes.font_size_codes_differ(source, text))
|
dropped_color = r"\f[14]Brothel"
|
||||||
|
self.assertTrue(wolf_codes.font_size_codes_differ(source, shrunk))
|
||||||
def test_names_doc_has_font_size_drift(self):
|
self.assertFalse(wolf_codes.font_size_codes_differ(source, dropped_color))
|
||||||
doc = {
|
self.assertTrue(
|
||||||
"kind": "names",
|
wolf_codes.names_doc_has_font_size_drift(
|
||||||
"names": [
|
{"kind": "names", "names": [{"source": source, "text": shrunk}]}
|
||||||
{
|
)
|
||||||
"source": r"\c[21]\f[20]娼館\c[19]\f[18]",
|
)
|
||||||
"text": r"\f[14]\c[21]\f[16]Brothel\c[19]\f[14]",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
self.assertTrue(wolf_codes.names_doc_has_font_size_drift(doc))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -137,34 +137,14 @@ class TestNameWrapRoles(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self._tmp.cleanup()
|
self._tmp.cleanup()
|
||||||
|
|
||||||
def test_roles_path_beside_names(self):
|
def test_upsert_load_and_update_name_wrap_role(self):
|
||||||
self.assertEqual(
|
|
||||||
wn.roles_json_path_for_names(self.names),
|
|
||||||
self.dir / "wolfdawn-roles.json",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_upsert_and_load_name_wrap_role(self):
|
|
||||||
roles_path = wn.roles_json_path_for_names(self.names)
|
roles_path = wn.roles_json_path_for_names(self.names)
|
||||||
wn.upsert_name_wrap_role(roles_path, "説明", width=48, max_lines=3)
|
self.assertEqual(roles_path, self.dir / "wolfdawn-roles.json")
|
||||||
roles = wn.load_name_wrap_roles(roles_path)
|
wn.upsert_name_wrap_role(roles_path, "説明", width=40, font=14)
|
||||||
role = wn.get_name_wrap_role(roles, "説明")
|
|
||||||
self.assertIsNotNone(role)
|
|
||||||
assert role is not None
|
|
||||||
self.assertEqual(role["width"], 48)
|
|
||||||
self.assertEqual(role["maxLines"], 3)
|
|
||||||
|
|
||||||
def test_upsert_updates_existing_note(self):
|
|
||||||
roles_path = wn.roles_json_path_for_names(self.names)
|
|
||||||
wn.upsert_name_wrap_role(roles_path, "説明", width=40)
|
|
||||||
wn.upsert_name_wrap_role(roles_path, "説明", width=52, max_lines=4)
|
wn.upsert_name_wrap_role(roles_path, "説明", width=52, max_lines=4)
|
||||||
role = wn.get_name_wrap_role(wn.load_name_wrap_roles(roles_path), "説明")
|
role = wn.get_name_wrap_role(wn.load_name_wrap_roles(roles_path), "説明")
|
||||||
self.assertEqual(role["width"], 52)
|
self.assertEqual(role["width"], 52)
|
||||||
self.assertEqual(role["maxLines"], 4)
|
self.assertEqual(role["maxLines"], 4)
|
||||||
|
|
||||||
def test_upsert_persists_font(self):
|
|
||||||
roles_path = wn.roles_json_path_for_names(self.names)
|
|
||||||
wn.upsert_name_wrap_role(roles_path, "説明", width=40, font=14)
|
|
||||||
role = wn.get_name_wrap_role(wn.load_name_wrap_roles(roles_path), "説明")
|
|
||||||
self.assertEqual(role["font"], 14)
|
self.assertEqual(role["font"], 14)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,21 +53,6 @@ RUMOR_FIXTURE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestWolfVisibleLength(unittest.TestCase):
|
|
||||||
def test_font_codes_do_not_count_toward_width(self):
|
|
||||||
plain = "Hello world"
|
|
||||||
coded = r"Hello \f[2]world"
|
|
||||||
self.assertEqual(
|
|
||||||
dazedwrap.max_line_visible_length(plain),
|
|
||||||
dazedwrap.max_line_visible_length(coded),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_gatekeeper_line_is_overflow_at_bulletin_width(self):
|
|
||||||
length = dazedwrap.max_line_visible_length(GATEKEEPER_TEXT)
|
|
||||||
self.assertGreater(length, 40)
|
|
||||||
self.assertGreater(length, 34)
|
|
||||||
|
|
||||||
|
|
||||||
class TestWrapSearch(unittest.TestCase):
|
class TestWrapSearch(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
|
@ -81,172 +66,29 @@ class TestWrapSearch(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.tmp.cleanup()
|
self.tmp.cleanup()
|
||||||
|
|
||||||
def test_search_finds_gatekeeper_in_rumor_sheet(self):
|
def test_search_and_wrap_db_sheet(self):
|
||||||
hits = ws.search_translated_text("gatekeeper just randomly", self.translated)
|
hits = ws.search_translated_text("gatekeeper just randomly", self.translated)
|
||||||
self.assertEqual(len(hits), 1)
|
self.assertEqual(len(hits), 1)
|
||||||
hit = hits[0]
|
hit = hits[0]
|
||||||
self.assertEqual(hit.kind, "db")
|
|
||||||
self.assertEqual(hit.sheet_name, RUMOR_SHEET)
|
self.assertEqual(hit.sheet_name, RUMOR_SHEET)
|
||||||
self.assertEqual(hit.row, GATEKEEPER_ROW)
|
self.assertEqual(hit.row, GATEKEEPER_ROW)
|
||||||
self.assertEqual(hit.field_name, GATEKEEPER_FIELD)
|
|
||||||
self.assertEqual(hit.json_file, "DataBase.project.json")
|
|
||||||
self.assertIn("gatekeeper", hit.text.lower())
|
|
||||||
|
|
||||||
def test_search_finds_horned_rabbits_same_row(self):
|
|
||||||
hits = ws.search_translated_text("horned rabbit", self.translated)
|
|
||||||
self.assertEqual(len(hits), 1)
|
|
||||||
self.assertEqual(hits[0].row, GATEKEEPER_ROW)
|
|
||||||
|
|
||||||
def test_wrap_overflow_in_sheet_changes_long_lines(self):
|
|
||||||
path = self.translated / "DataBase.project.json"
|
path = self.translated / "DataBase.project.json"
|
||||||
doc = json.loads(path.read_text(encoding="utf-8"))
|
doc = json.loads(path.read_text(encoding="utf-8"))
|
||||||
changed = ws.wrap_overflow_in_sheet(path, doc, RUMOR_SHEET, width=34)
|
changed = ws.wrap_overflow_in_sheet(path, doc, RUMOR_SHEET, width=34)
|
||||||
self.assertEqual(changed, 1)
|
self.assertEqual(changed, 1)
|
||||||
doc2 = json.loads(path.read_text(encoding="utf-8"))
|
line = ws.locate_line(doc, hit.hit_id)
|
||||||
line = ws.locate_line(
|
|
||||||
doc2,
|
|
||||||
{
|
|
||||||
"kind": "db",
|
|
||||||
"sheet_name": RUMOR_SHEET,
|
|
||||||
"row": GATEKEEPER_ROW,
|
|
||||||
"field_name": GATEKEEPER_FIELD,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertIsNotNone(line)
|
|
||||||
assert line is not None
|
assert line is not None
|
||||||
self.assertIn("\n", line["text"])
|
self.assertIn("\n", line["text"])
|
||||||
self.assertLessEqual(dazedwrap.max_line_visible_length(line["text"]), 34)
|
self.assertLessEqual(dazedwrap.max_line_visible_length(line["text"]), 34)
|
||||||
|
|
||||||
def test_wrap_profile_remembers_sheet_width(self):
|
|
||||||
ws.set_sheet_width(self.work, RUMOR_SHEET, 34, json_file="DataBase.project.json")
|
ws.set_sheet_width(self.work, RUMOR_SHEET, 34, json_file="DataBase.project.json")
|
||||||
profile = ws.load_wrap_profile(self.work)
|
self.assertEqual(
|
||||||
self.assertEqual(ws.get_sheet_width(profile, RUMOR_SHEET), 34)
|
ws.get_sheet_width(ws.load_wrap_profile(self.work), RUMOR_SHEET), 34
|
||||||
entry = profile["sheets"][RUMOR_SHEET]
|
|
||||||
self.assertEqual(entry["json_file"], "DataBase.project.json")
|
|
||||||
|
|
||||||
def test_sheet_overflow_summary_counts_gatekeeper_row(self):
|
|
||||||
summaries = ws.sheet_overflow_summaries(self.translated, width=34)
|
|
||||||
rumor = [s for s in summaries if s.sheet_name == RUMOR_SHEET]
|
|
||||||
self.assertEqual(len(rumor), 1)
|
|
||||||
self.assertEqual(rumor[0].overflow_count, 1)
|
|
||||||
self.assertEqual(rumor[0].line_count, 2)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNamesSearch(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
|
||||||
self.translated = Path(self.tmp.name) / "translated"
|
|
||||||
self.translated.mkdir()
|
|
||||||
fixture = {
|
|
||||||
"kind": "names",
|
|
||||||
"count": 1,
|
|
||||||
"names": [
|
|
||||||
{
|
|
||||||
"source": "ブロthel scout line",
|
|
||||||
"text": (
|
|
||||||
"Maybe I'll go scout out some newcomers at the brothel.\n"
|
|
||||||
"\u3000Hope there's a pure-looking girl with big boobs..."
|
|
||||||
),
|
|
||||||
"note": "Profile · プロフィール",
|
|
||||||
"safety": "safe",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
(self.translated / "names.json").write_text(
|
|
||||||
json.dumps(fixture, ensure_ascii=False, indent=4),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.tmp.cleanup()
|
|
||||||
|
|
||||||
def test_search_finds_names_json_profile_text(self):
|
class TestNamesSearchAndWrap(unittest.TestCase):
|
||||||
hits = ws.search_translated_text("there's a pure", self.translated)
|
|
||||||
self.assertEqual(len(hits), 1)
|
|
||||||
hit = hits[0]
|
|
||||||
self.assertEqual(hit.kind, "names")
|
|
||||||
self.assertEqual(hit.json_file, "names.json")
|
|
||||||
self.assertIn("pure-looking", hit.text)
|
|
||||||
|
|
||||||
def test_load_hit_from_names_index(self):
|
|
||||||
hits = ws.search_translated_text("pure-looking", self.translated)
|
|
||||||
self.assertEqual(len(hits), 1)
|
|
||||||
path, doc, line = ws.load_hit_from_id(self.translated, hits[0].hit_id)
|
|
||||||
self.assertIsNotNone(path)
|
|
||||||
assert path is not None and line is not None
|
|
||||||
self.assertEqual(path.name, "names.json")
|
|
||||||
self.assertIn("pure-looking", line["text"])
|
|
||||||
|
|
||||||
|
|
||||||
class TestWrapPreview(unittest.TestCase):
|
|
||||||
SAMPLE = (
|
|
||||||
'"Apparently there\'s a super hot female adventurer who just '
|
|
||||||
'came to town!? Maybe I can meet her if I head to the '
|
|
||||||
r'\c[21]\f[20]tavern\c[19]\f[18]!"'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preview_detects_wrap_needed(self):
|
|
||||||
info = ws.wrap_preview_info(self.SAMPLE, 40)
|
|
||||||
self.assertTrue(info["needs_wrap"])
|
|
||||||
self.assertGreater(info["longest"], 40)
|
|
||||||
self.assertGreater(info["output_line_count"], 1)
|
|
||||||
|
|
||||||
def test_format_preview_shows_line_counts(self):
|
|
||||||
preview = ws.format_wrap_preview(self.SAMPLE, 40)
|
|
||||||
self.assertIn("(40)", preview)
|
|
||||||
self.assertIn("Apparently", preview)
|
|
||||||
|
|
||||||
def test_split_line_at_visible_width(self):
|
|
||||||
line = "Apparently there's a super hot female adventurer who just"
|
|
||||||
fit_end, line_len = ws.split_line_at_visible_width(line, 40)
|
|
||||||
self.assertEqual(line_len, len(line))
|
|
||||||
self.assertGreater(fit_end, 0)
|
|
||||||
self.assertLess(fit_end, line_len)
|
|
||||||
self.assertLessEqual(
|
|
||||||
dazedwrap.max_line_visible_length(line[:fit_end]),
|
|
||||||
40,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_codes_do_not_inflate_visible_counts(self):
|
|
||||||
text = r'See the \c[21]\f[20]tavern\c[19]\f[18] on Main Street'
|
|
||||||
info = ws.wrap_preview_info(text, 30)
|
|
||||||
self.assertLess(info["longest"], len(text))
|
|
||||||
self.assertLessEqual(info["longest"], 30 + 10)
|
|
||||||
|
|
||||||
def test_render_wolf_text_html_hides_codes_and_styles(self):
|
|
||||||
text = r'That \c[21]\f[20]cafe\c[19]\f[18] over there'
|
|
||||||
html = ws.render_wolf_text_html(text, default_font_px=18)
|
|
||||||
self.assertNotIn(r"\c[", html)
|
|
||||||
self.assertNotIn(r"\f[", html)
|
|
||||||
self.assertIn("cafe", html)
|
|
||||||
self.assertIn("font-size:20px", html)
|
|
||||||
self.assertIn("font-size:18px", html)
|
|
||||||
# Emphasis colour for \\c[21]
|
|
||||||
self.assertIn(ws.wolf_preview_color(21), html)
|
|
||||||
|
|
||||||
def test_format_wrap_preview_html_includes_gutters(self):
|
|
||||||
text = (
|
|
||||||
r'\f[18]"That \c[21]\f[20]cafe\c[19]\f[18] over there has been'
|
|
||||||
'\npacked lately."'
|
|
||||||
)
|
|
||||||
html = ws.format_wrap_preview_html(text, 40)
|
|
||||||
self.assertIn("(37)", html)
|
|
||||||
self.assertIn("cafe", html)
|
|
||||||
self.assertNotIn(r"\f[18]", html)
|
|
||||||
self.assertIn("font-size:20px", html)
|
|
||||||
self.assertIn(ws.wolf_preview_color(21), html)
|
|
||||||
|
|
||||||
def test_format_wrap_preview_html_simulates_body_font(self):
|
|
||||||
text = r'That \c[21]\f[20]cafe\c[19]\f[18] over there'
|
|
||||||
html = ws.format_wrap_preview_html(text, 40, body_font=13)
|
|
||||||
# 20*13/18 ≈ 14; body 13
|
|
||||||
self.assertIn("font-size:14px", html)
|
|
||||||
self.assertIn("font-size:13px", html)
|
|
||||||
self.assertNotIn("font-size:20px", html)
|
|
||||||
self.assertNotIn("font-size:18px", html)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNamesCategoryWrap(unittest.TestCase):
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
self.translated = Path(self.tmp.name) / "translated"
|
self.translated = Path(self.tmp.name) / "translated"
|
||||||
|
|
@ -254,11 +96,10 @@ class TestNamesCategoryWrap(unittest.TestCase):
|
||||||
self.category = "├■街の噂(MOB)"
|
self.category = "├■街の噂(MOB)"
|
||||||
fixture = {
|
fixture = {
|
||||||
"kind": "names",
|
"kind": "names",
|
||||||
"count": 2,
|
|
||||||
"names": [
|
"names": [
|
||||||
{
|
{
|
||||||
"source": "短い",
|
"source": "「メイドがどんどん辞めていく」",
|
||||||
"text": "Short line.",
|
"text": '"Yeah, maids keep quitting one after another."',
|
||||||
"note": self.category,
|
"note": self.category,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -277,107 +118,44 @@ class TestNamesCategoryWrap(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.tmp.cleanup()
|
self.tmp.cleanup()
|
||||||
|
|
||||||
def test_names_categories_in_sheet_summaries(self):
|
def test_search_summary_shows_translated_text(self):
|
||||||
summaries = ws.sheet_overflow_summaries(self.translated, width=34)
|
hits = ws.search_translated_text("maids", self.translated)
|
||||||
names_rows = [s for s in summaries if s.kind == "names" and s.sheet_name == self.category]
|
self.assertEqual(len(hits), 1)
|
||||||
self.assertEqual(len(names_rows), 1)
|
summary = hits[0].summary(40)
|
||||||
self.assertEqual(names_rows[0].line_count, 2)
|
self.assertTrue(summary.split("\n", 1)[0].startswith('"Yeah, maids'))
|
||||||
self.assertEqual(names_rows[0].overflow_count, 1)
|
self.assertNotIn("メイド", summary)
|
||||||
|
path, _doc, line = ws.load_hit_from_id(self.translated, hits[0].hit_id)
|
||||||
|
assert path is not None and line is not None
|
||||||
|
self.assertEqual(path.name, "names.json")
|
||||||
|
self.assertIn("maids", line["text"])
|
||||||
|
|
||||||
def test_wrap_all_in_names_category(self):
|
def test_wrap_overflow_in_names_category(self):
|
||||||
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
||||||
changed = ws.wrap_overflow_in_sheet(
|
changed = ws.wrap_overflow_in_sheet(
|
||||||
self.path, doc, self.category, 34, kind="names",
|
self.path, doc, self.category, 34, kind="names",
|
||||||
)
|
)
|
||||||
self.assertEqual(changed, 1)
|
self.assertGreaterEqual(changed, 1)
|
||||||
doc2 = json.loads(self.path.read_text(encoding="utf-8"))
|
long_text = json.loads(self.path.read_text(encoding="utf-8"))["names"][1]["text"]
|
||||||
long_entry = doc2["names"][1]["text"]
|
self.assertIn("\n", long_text)
|
||||||
self.assertIn("\n", long_entry)
|
self.assertLessEqual(dazedwrap.max_line_visible_length(long_text), 34)
|
||||||
|
|
||||||
def test_names_search_summary_shows_translated_text(self):
|
|
||||||
hits = ws.search_translated_text("maids", self.translated)
|
|
||||||
# Seed a maid line so EN query matches EN text, not JP source.
|
|
||||||
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
|
||||||
doc["names"].append(
|
|
||||||
{
|
|
||||||
"source": "「メイドがどんどん辞めていく」",
|
|
||||||
"text": '"Yeah, maids keep quitting one after another."',
|
|
||||||
"note": self.category,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.path.write_text(json.dumps(doc, ensure_ascii=False, indent=4), encoding="utf-8")
|
|
||||||
hits = ws.search_translated_text("maids", self.translated)
|
|
||||||
self.assertEqual(len(hits), 1)
|
|
||||||
summary = hits[0].summary(40)
|
|
||||||
self.assertIn("maids", summary)
|
|
||||||
self.assertNotIn("メイド", summary)
|
|
||||||
self.assertIn("entry #2", summary)
|
|
||||||
# Translated preview is the first line of the summary.
|
|
||||||
self.assertIn("\n", summary)
|
|
||||||
self.assertTrue(summary.split("\n", 1)[0].strip().startswith('"Yeah'))
|
|
||||||
|
|
||||||
|
|
||||||
class TestEventScopeWrap(unittest.TestCase):
|
class TestWrapPreviewHtml(unittest.TestCase):
|
||||||
def setUp(self):
|
def test_html_preview_hides_codes_and_simulates_body_font(self):
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
text = r'That \c[21]\f[20]cafe\c[19]\f[18] over there'
|
||||||
self.translated = Path(self.tmp.name) / "translated"
|
html = ws.format_wrap_preview_html(text, 40, body_font=13)
|
||||||
self.translated.mkdir()
|
self.assertNotIn(r"\c[", html)
|
||||||
self.path = self.translated / "Map001.mps.json"
|
self.assertNotIn(r"\f[", html)
|
||||||
self.path.write_text(
|
self.assertIn("cafe", html)
|
||||||
json.dumps(
|
self.assertIn("font-size:14px", html) # 20*13/18
|
||||||
{
|
self.assertIn("font-size:13px", html)
|
||||||
"kind": "map",
|
self.assertIn(ws.wolf_preview_color(21), html)
|
||||||
"file": "Map001.mps",
|
info = ws.wrap_preview_info(text, 40)
|
||||||
"scenes": [
|
self.assertLess(info["longest"], len(text))
|
||||||
{
|
|
||||||
"event": 1,
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"text": (
|
|
||||||
"This is a very long line of dialogue that "
|
|
||||||
"should wrap at thirty four visible chars."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{"text": "Short line."},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ensure_ascii=False,
|
|
||||||
indent=4,
|
|
||||||
),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
self.hit = ws.WrapHit(
|
|
||||||
json_file="Map001.mps.json",
|
|
||||||
kind="map",
|
|
||||||
sheet_name="Map001.mps",
|
|
||||||
row=1,
|
|
||||||
field_name="scene 0 line 0",
|
|
||||||
text="very long line",
|
|
||||||
max_line_len=80,
|
|
||||||
scene_index=0,
|
|
||||||
line_index=0,
|
|
||||||
map_file="Map001.mps",
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.tmp.cleanup()
|
|
||||||
|
|
||||||
def test_scope_stats_for_map_file(self):
|
|
||||||
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
|
||||||
stats = ws.scope_stats(doc, self.hit, 34)
|
|
||||||
self.assertEqual(stats.total, 2)
|
|
||||||
self.assertEqual(stats.overflow, 1)
|
|
||||||
|
|
||||||
def test_wrap_all_in_map_file(self):
|
|
||||||
doc = json.loads(self.path.read_text(encoding="utf-8"))
|
|
||||||
changed = ws.wrap_overflow_in_scope(self.path, doc, self.hit, 34)
|
|
||||||
self.assertEqual(changed, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class TestManualFontAndWrap(unittest.TestCase):
|
class TestManualFontAndWrap(unittest.TestCase):
|
||||||
def test_scales_emphasis_then_wraps(self):
|
def test_manual_scales_emphasis_wraps_and_skips_short_plain(self):
|
||||||
text = (
|
text = (
|
||||||
r"So they even take worries like this in the "
|
r"So they even take worries like this in the "
|
||||||
r"\c[21]\f[20]confessional\c[19]\f[18] booth every day."
|
r"\c[21]\f[20]confessional\c[19]\f[18] booth every day."
|
||||||
|
|
@ -388,18 +166,15 @@ class TestManualFontAndWrap(unittest.TestCase):
|
||||||
self.assertIn(r"\f[14]", new_text)
|
self.assertIn(r"\f[14]", new_text)
|
||||||
self.assertLessEqual(dazedwrap.max_line_visible_length(new_text), 40)
|
self.assertLessEqual(dazedwrap.max_line_visible_length(new_text), 40)
|
||||||
|
|
||||||
def test_group_manual_skips_short_plain_lines(self):
|
short, skipped = ws.apply_manual_font_and_wrap(
|
||||||
text = "Short."
|
"Short.", 40, font=14, only_overflow_or_fonts=True
|
||||||
new_text, changed = ws.apply_manual_font_and_wrap(
|
|
||||||
text, 40, font=14, only_overflow_or_fonts=True
|
|
||||||
)
|
)
|
||||||
self.assertFalse(changed)
|
self.assertFalse(skipped)
|
||||||
self.assertEqual(new_text, text)
|
self.assertEqual(short, "Short.")
|
||||||
|
|
||||||
def test_wrap_overflow_manual_in_names_category(self):
|
def test_wrap_overflow_manual_in_names_category(self):
|
||||||
tmp = tempfile.TemporaryDirectory()
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
try:
|
path = Path(tmp) / "names.json"
|
||||||
path = Path(tmp.name) / "names.json"
|
|
||||||
doc = {
|
doc = {
|
||||||
"kind": "names",
|
"kind": "names",
|
||||||
"names": [
|
"names": [
|
||||||
|
|
@ -428,12 +203,48 @@ class TestManualFontAndWrap(unittest.TestCase):
|
||||||
changed = ws.wrap_overflow_manual_in_scope(path, doc, hit, 36, font=14)
|
changed = ws.wrap_overflow_manual_in_scope(path, doc, hit, 36, font=14)
|
||||||
self.assertEqual(changed, 1)
|
self.assertEqual(changed, 1)
|
||||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||||
out = saved["names"][0]["text"]
|
self.assertIn(r"\f[14]", saved["names"][0]["text"])
|
||||||
self.assertIn(r"\f[14]", out)
|
|
||||||
self.assertEqual(saved["names"][1]["text"], "OK")
|
self.assertEqual(saved["names"][1]["text"], "OK")
|
||||||
self.assertEqual(saved["names"][2]["text"], "x" * 80)
|
self.assertEqual(saved["names"][2]["text"], "x" * 80)
|
||||||
finally:
|
|
||||||
tmp.cleanup()
|
|
||||||
|
class TestEventScopeWrap(unittest.TestCase):
|
||||||
|
def test_wrap_all_in_map_file(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp) / "Map001.mps.json"
|
||||||
|
doc = {
|
||||||
|
"kind": "map",
|
||||||
|
"file": "Map001.mps",
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"event": 1,
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"text": (
|
||||||
|
"This is a very long line of dialogue that "
|
||||||
|
"should wrap at thirty four visible chars."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{"text": "Short line."},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
|
||||||
|
hit = ws.WrapHit(
|
||||||
|
json_file="Map001.mps.json",
|
||||||
|
kind="map",
|
||||||
|
sheet_name="Map001.mps",
|
||||||
|
row=1,
|
||||||
|
field_name="scene 0 line 0",
|
||||||
|
text="very long line",
|
||||||
|
max_line_len=80,
|
||||||
|
scene_index=0,
|
||||||
|
line_index=0,
|
||||||
|
map_file="Map001.mps",
|
||||||
|
)
|
||||||
|
self.assertEqual(ws.scope_stats(doc, hit, 34).overflow, 1)
|
||||||
|
self.assertEqual(ws.wrap_overflow_in_scope(path, doc, hit, 34), 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Unit tests for WolfDawn relayout / desc_relayout CLI wrappers."""
|
"""Unit tests for WolfDawn relayout / names-wrap CLI wrappers."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ from util import wolfdawn # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class TestRelayoutWrappers(unittest.TestCase):
|
class TestRelayoutWrappers(unittest.TestCase):
|
||||||
def test_relayout_dry_run_args(self):
|
def test_relayout_auto_metrics(self):
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
def fake_run(args, log_fn=None):
|
||||||
|
|
@ -25,40 +25,18 @@ class TestRelayoutWrappers(unittest.TestCase):
|
||||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||||
wolfdawn.relayout("/game/Data", width=55, max_rows=0)
|
wolfdawn.relayout("/game/Data", width="auto", max_rows=0, out_dir="/tmp/out")
|
||||||
self.assertEqual(captured["args"][:2], ["relayout", "/game/Data"])
|
|
||||||
self.assertIn("--width", captured["args"])
|
|
||||||
self.assertIn("55", captured["args"])
|
|
||||||
self.assertNotIn("-o", captured["args"])
|
|
||||||
self.assertIn("--max-rows", captured["args"])
|
|
||||||
self.assertIn("auto", captured["args"])
|
|
||||||
|
|
||||||
def test_relayout_write_args(self):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
|
||||||
captured["args"] = list(args)
|
|
||||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
|
||||||
wolfdawn.relayout(
|
|
||||||
"/game/Data",
|
|
||||||
out_dir="/tmp/out",
|
|
||||||
width=60,
|
|
||||||
max_rows=4,
|
|
||||||
no_resolve=True,
|
|
||||||
)
|
|
||||||
args = captured["args"]
|
args = captured["args"]
|
||||||
self.assertEqual(args[0], "relayout")
|
self.assertEqual(args[:2], ["relayout", "/game/Data"])
|
||||||
self.assertIn("-o", args)
|
self.assertIn("-o", args)
|
||||||
self.assertIn("/tmp/out", args)
|
self.assertIn("/tmp/out", args)
|
||||||
self.assertIn("--width", args)
|
self.assertIn("--width", args)
|
||||||
self.assertIn("60", args)
|
self.assertIn("auto", args)
|
||||||
self.assertIn("--max-rows", args)
|
self.assertIn("--max-rows", args)
|
||||||
self.assertIn("4", args)
|
# max_rows=0 becomes auto
|
||||||
self.assertIn("--no-resolve", args)
|
self.assertEqual(args[args.index("--max-rows") + 1], "auto")
|
||||||
|
|
||||||
def test_desc_relayout_args(self):
|
def test_desc_relayout_auto_max_lines(self):
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
def fake_run(args, log_fn=None):
|
||||||
|
|
@ -75,88 +53,32 @@ class TestRelayoutWrappers(unittest.TestCase):
|
||||||
)
|
)
|
||||||
args = captured["args"]
|
args = captured["args"]
|
||||||
self.assertEqual(args[0], "desc-relayout")
|
self.assertEqual(args[0], "desc-relayout")
|
||||||
self.assertIn("-o", args)
|
|
||||||
self.assertIn("/tmp/DataBase.project", args)
|
|
||||||
self.assertIn("--width", args)
|
|
||||||
self.assertIn("75", args)
|
|
||||||
self.assertIn("--max-lines", args)
|
self.assertIn("--max-lines", args)
|
||||||
self.assertIn("auto", args)
|
self.assertEqual(args[args.index("--max-lines") + 1], "auto")
|
||||||
self.assertIn("--keep-breaks", args)
|
self.assertIn("--keep-breaks", args)
|
||||||
|
|
||||||
def test_relayout_auto_width(self):
|
def test_names_wrap_flags_and_parsers(self):
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
|
||||||
captured["args"] = list(args)
|
|
||||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
|
||||||
wolfdawn.relayout("/game/Data", width="auto", max_rows="auto")
|
|
||||||
args = captured["args"]
|
|
||||||
self.assertIn("--width", args)
|
|
||||||
self.assertIn("auto", args)
|
|
||||||
self.assertIn("--max-rows", args)
|
|
||||||
|
|
||||||
def test_names_wrap_args(self):
|
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
def fake_run(args, log_fn=None):
|
||||||
captured["args"] = list(args)
|
captured["args"] = list(args)
|
||||||
return wolfdawn.WolfResult(
|
return wolfdawn.WolfResult(
|
||||||
0,
|
0,
|
||||||
"names-wrap: 3 entry(ies) re-wrapped",
|
' shrunk to \\f[14] to fit its slot: "Hey! ..."\n'
|
||||||
|
' shrunk to \\f[12] to fit its slot: "That sister..."\n'
|
||||||
|
"names-wrap: 7 entry(ies) re-wrapped",
|
||||||
"",
|
"",
|
||||||
[str(a) for a in args],
|
[str(a) for a in args],
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
||||||
res = wolfdawn.names_wrap("/tmp/names.json")
|
res = wolfdawn.names_wrap(
|
||||||
self.assertEqual(captured["args"], ["names-wrap", "/tmp/names.json"])
|
|
||||||
self.assertEqual(wolfdawn.parse_names_wrap_counts(res.stdout), 3)
|
|
||||||
|
|
||||||
def test_names_wrap_dry_run(self):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
|
||||||
captured["args"] = list(args)
|
|
||||||
return wolfdawn.WolfResult(
|
|
||||||
0,
|
|
||||||
"names-wrap: 2 entry(ies) WOULD be re-wrapped",
|
|
||||||
"",
|
|
||||||
[str(a) for a in args],
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
|
||||||
wolfdawn.names_wrap("/tmp/names.json", dry_run=True)
|
|
||||||
self.assertEqual(captured["args"], ["names-wrap", "/tmp/names.json", "--dry-run"])
|
|
||||||
|
|
||||||
def test_names_wrap_note_and_width(self):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
|
||||||
captured["args"] = list(args)
|
|
||||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
|
||||||
wolfdawn.names_wrap(
|
|
||||||
"/tmp/names.json",
|
"/tmp/names.json",
|
||||||
note="├■街の噂(MOB)",
|
note="説明",
|
||||||
width=66,
|
width=50,
|
||||||
|
lines=3,
|
||||||
|
dry_run=True,
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
|
||||||
captured["args"],
|
|
||||||
["names-wrap", "/tmp/names.json", "--width", "66", "--note", "├■街の噂(MOB)"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_names_wrap_lines(self):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_run(args, log_fn=None):
|
|
||||||
captured["args"] = list(args)
|
|
||||||
return wolfdawn.WolfResult(0, "", "", [str(a) for a in args])
|
|
||||||
|
|
||||||
with patch.object(wolfdawn, "_run", side_effect=fake_run):
|
|
||||||
wolfdawn.names_wrap("/tmp/names.json", width=50, lines=3, note="説明")
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
captured["args"],
|
captured["args"],
|
||||||
[
|
[
|
||||||
|
|
@ -168,16 +90,11 @@ class TestRelayoutWrappers(unittest.TestCase):
|
||||||
"3",
|
"3",
|
||||||
"--note",
|
"--note",
|
||||||
"説明",
|
"説明",
|
||||||
|
"--dry-run",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
self.assertEqual(wolfdawn.parse_names_wrap_counts(res.stdout), 7)
|
||||||
def test_parse_names_wrap_shrink_count(self):
|
self.assertEqual(wolfdawn.parse_names_wrap_shrink_count(res.stdout), 2)
|
||||||
blob = (
|
|
||||||
' shrunk to \\f[14] to fit its slot: "Hey! ..."\n'
|
|
||||||
' shrunk to \\f[12] to fit its slot: "That sister..."\n'
|
|
||||||
"names-wrap: 7 entry(ies) re-wrapped"
|
|
||||||
)
|
|
||||||
self.assertEqual(wolfdawn.parse_names_wrap_shrink_count(blob), 2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -134,27 +134,6 @@ def scale_font_sizes(
|
||||||
return new_text, count
|
return new_text, count
|
||||||
|
|
||||||
|
|
||||||
def apply_font_size(text: str, size: int) -> tuple[str, int]:
|
|
||||||
"""Force every ``\\f[N]`` in *text* to the same *size* (no proportional scale).
|
|
||||||
|
|
||||||
Prefer :func:`scale_font_sizes` when the line has emphasis overrides that
|
|
||||||
should stay relatively larger than the body font.
|
|
||||||
|
|
||||||
Always ensures a leading ``\\f[size]``. Returns ``(new_text, replacements)``.
|
|
||||||
"""
|
|
||||||
if not isinstance(text, str) or not text.strip() or size <= 0:
|
|
||||||
return text, 0
|
|
||||||
size = int(size)
|
|
||||||
matches = list(WOLF_FONT_SIZE_RE.finditer(text))
|
|
||||||
if matches:
|
|
||||||
had_leading = bool(WOLF_FONT_SIZE_RE.match(text))
|
|
||||||
new_text = WOLF_FONT_SIZE_RE.sub(rf"\\f[{size}]", text)
|
|
||||||
new_text = ensure_leading_font_size(new_text, size)
|
|
||||||
count = len(matches) + (0 if had_leading else 1)
|
|
||||||
return new_text, count
|
|
||||||
return ensure_leading_font_size(text, size), 1
|
|
||||||
|
|
||||||
|
|
||||||
def _control_code_tokens(text: str) -> list[str]:
|
def _control_code_tokens(text: str) -> list[str]:
|
||||||
"""Return the sequence of WolfDawn-compared control codes in *text*."""
|
"""Return the sequence of WolfDawn-compared control codes in *text*."""
|
||||||
if not isinstance(text, str) or not text:
|
if not isinstance(text, str) or not text:
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Sequence
|
from typing import Any, Sequence
|
||||||
|
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
from util.wolfdawn.db_classify import group_key, json_file_from_doc
|
|
||||||
from util.wolfdawn.selective_wrap import line_needs_wrap, wrap_line_text
|
from util.wolfdawn.selective_wrap import line_needs_wrap, wrap_line_text
|
||||||
|
|
||||||
WRAP_PROFILE_NAME = "wrap_profile.json"
|
WRAP_PROFILE_NAME = "wrap_profile.json"
|
||||||
|
|
@ -89,22 +88,6 @@ class WrapHit:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SheetOverflowSummary:
|
|
||||||
json_file: str
|
|
||||||
sheet_name: str
|
|
||||||
line_count: int
|
|
||||||
overflow_count: int
|
|
||||||
tier: str = ""
|
|
||||||
kind: str = "db"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key(self) -> str:
|
|
||||||
if self.kind == "names":
|
|
||||||
return f"{self.json_file}|names|{self.sheet_name}"
|
|
||||||
return group_key(self.json_file, self.sheet_name)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_profile_path(work_dir: str | Path) -> Path:
|
def wrap_profile_path(work_dir: str | Path) -> Path:
|
||||||
return Path(work_dir) / WRAP_PROFILE_NAME
|
return Path(work_dir) / WRAP_PROFILE_NAME
|
||||||
|
|
||||||
|
|
@ -402,20 +385,6 @@ def wrap_preview_info(text: str, width: int) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_wrap_preview(text: str, width: int) -> str:
|
|
||||||
"""Multiline preview with per-line visible character counts."""
|
|
||||||
info = wrap_preview_info(text, width)
|
|
||||||
if not info["wrapped"]:
|
|
||||||
return ""
|
|
||||||
lines = info["wrapped"].replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
||||||
parts: list[str] = []
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
vis = dazedwrap.max_line_visible_length(line)
|
|
||||||
marker = "⚠" if vis > width else " "
|
|
||||||
parts.append(f"{marker} {i + 1:2d} ({vis:2d}) {line}")
|
|
||||||
return "\n".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_preview_summary(text: str, width: int) -> str:
|
def wrap_preview_summary(text: str, width: int) -> str:
|
||||||
"""One-line status for the preview header."""
|
"""One-line status for the preview header."""
|
||||||
info = wrap_preview_info(text, width)
|
info = wrap_preview_info(text, width)
|
||||||
|
|
@ -578,15 +547,6 @@ def format_wrap_preview_html(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def split_line_at_visible_width(line: str, width: int) -> tuple[int, int]:
|
|
||||||
"""Return ``(fit_end, line_len)`` char indices in *line* for UI highlighting."""
|
|
||||||
line_len = len(line)
|
|
||||||
if not line or width <= 0:
|
|
||||||
return (line_len, line_len)
|
|
||||||
fit_end = dazedwrap.visible_word_wrap_end_index(line, width)
|
|
||||||
return (min(fit_end, line_len), line_len)
|
|
||||||
|
|
||||||
|
|
||||||
def locate_line(doc: dict[str, Any], hit_id: dict[str, Any]) -> dict[str, Any] | None:
|
def locate_line(doc: dict[str, Any], hit_id: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
"""Return the live line dict for *hit_id* inside *doc*."""
|
"""Return the live line dict for *hit_id* inside *doc*."""
|
||||||
kind = hit_id.get("kind")
|
kind = hit_id.get("kind")
|
||||||
|
|
@ -1081,96 +1041,3 @@ def _wrap_overflow_in_names_category(
|
||||||
if changed:
|
if changed:
|
||||||
save_document(path, doc)
|
save_document(path, doc)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def _names_category_summaries(
|
|
||||||
doc: dict[str, Any],
|
|
||||||
*,
|
|
||||||
json_file: str,
|
|
||||||
width: int,
|
|
||||||
) -> list[SheetOverflowSummary]:
|
|
||||||
counts: dict[str, int] = {}
|
|
||||||
overflow: dict[str, int] = {}
|
|
||||||
for entry in doc.get("names") or []:
|
|
||||||
if not isinstance(entry, dict):
|
|
||||||
continue
|
|
||||||
note = str(entry.get("note") or "names.json")
|
|
||||||
counts[note] = counts.get(note, 0) + 1
|
|
||||||
text = entry.get("text")
|
|
||||||
if isinstance(text, str) and line_needs_wrap(text, width):
|
|
||||||
overflow[note] = overflow.get(note, 0) + 1
|
|
||||||
return [
|
|
||||||
SheetOverflowSummary(
|
|
||||||
json_file=json_file,
|
|
||||||
sheet_name=note,
|
|
||||||
line_count=counts[note],
|
|
||||||
overflow_count=overflow.get(note, 0),
|
|
||||||
tier="names",
|
|
||||||
kind="names",
|
|
||||||
)
|
|
||||||
for note in sorted(counts)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def sheet_overflow_summaries(
|
|
||||||
translated_dir: str | Path,
|
|
||||||
width: int,
|
|
||||||
*,
|
|
||||||
files_dir: str | Path | None = None,
|
|
||||||
) -> list[SheetOverflowSummary]:
|
|
||||||
"""Per DB sheet overflow counts at *width*."""
|
|
||||||
from util.wolfdawn.db_classify import analyze_content_distribution, classify_db_document
|
|
||||||
|
|
||||||
base = Path(translated_dir)
|
|
||||||
if not base.is_dir():
|
|
||||||
base = Path(files_dir) if files_dir else base
|
|
||||||
dist = analyze_content_distribution(base)
|
|
||||||
summaries: list[SheetOverflowSummary] = []
|
|
||||||
|
|
||||||
for path in sorted(base.glob("*.project.json")):
|
|
||||||
doc = _load_json(path)
|
|
||||||
if not doc or doc.get("kind") != "db":
|
|
||||||
continue
|
|
||||||
jf = path.name
|
|
||||||
for group in classify_db_document(doc, json_file=jf):
|
|
||||||
overflow = 0
|
|
||||||
for g in doc.get("groups") or []:
|
|
||||||
if str(g.get("typeName") or "") != group.type_name:
|
|
||||||
continue
|
|
||||||
for line in g.get("lines") or []:
|
|
||||||
text = line.get("text")
|
|
||||||
if isinstance(text, str) and line_needs_wrap(text, width):
|
|
||||||
overflow += 1
|
|
||||||
summaries.append(
|
|
||||||
SheetOverflowSummary(
|
|
||||||
json_file=jf,
|
|
||||||
sheet_name=group.type_name,
|
|
||||||
line_count=group.line_count,
|
|
||||||
overflow_count=overflow,
|
|
||||||
tier=group.tier,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not summaries:
|
|
||||||
summaries = [
|
|
||||||
SheetOverflowSummary(
|
|
||||||
json_file=g.json_file,
|
|
||||||
sheet_name=g.type_name,
|
|
||||||
line_count=g.line_count,
|
|
||||||
overflow_count=0,
|
|
||||||
tier=g.tier,
|
|
||||||
)
|
|
||||||
for g in dist.groups
|
|
||||||
]
|
|
||||||
|
|
||||||
seen_names: set[Path] = set()
|
|
||||||
for search_base in _iter_search_dirs(base, Path(files_dir) if files_dir else None):
|
|
||||||
names_path = search_base / "names.json"
|
|
||||||
if not names_path.is_file() or names_path in seen_names:
|
|
||||||
continue
|
|
||||||
seen_names.add(names_path)
|
|
||||||
doc = _load_json(names_path)
|
|
||||||
if doc and doc.get("kind") == "names":
|
|
||||||
summaries.extend(
|
|
||||||
_names_category_summaries(doc, json_file="names.json", width=width)
|
|
||||||
)
|
|
||||||
return summaries
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue