Trim tests

This commit is contained in:
DazedAnon 2026-07-09 07:29:20 -05:00
parent 15eac9b1bf
commit eca44a15bd
6 changed files with 129 additions and 637 deletions

View file

@ -1,5 +1,5 @@
#!/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
@ -59,95 +59,33 @@ class WolfCodesRepairTests(unittest.TestCase):
"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]!"
new_text, count = wolf_codes.apply_font_size(text, 16)
self.assertEqual(count, 3) # 2 replaced + leading body
self.assertTrue(new_text.startswith(r"\f[16]"))
self.assertIn(r"\f[16]", new_text)
class WolfFontScaleTests(unittest.TestCase):
def test_scale_keeps_emphasis_ratio_and_leads_with_body(self):
# Cafe-style mid-line emphasis: body must lead so Wolf sets line height.
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[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)
def test_scale_font_sizes_keeps_emphasis_ratio(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):
def test_font_size_codes_differ_only_for_size_drift(self):
source = r"\c[21]\f[20]娼館\c[19]\f[18]"
text = r"\f[14]Brothel" # dropped colour codes
self.assertFalse(wolf_codes.font_size_codes_differ(source, text))
def test_names_doc_has_font_size_drift(self):
doc = {
"kind": "names",
"names": [
{
"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))
shrunk = r"\f[14]\c[21]\f[16]Brothel\c[19]\f[14]"
dropped_color = r"\f[14]Brothel"
self.assertTrue(wolf_codes.font_size_codes_differ(source, shrunk))
self.assertFalse(wolf_codes.font_size_codes_differ(source, dropped_color))
self.assertTrue(
wolf_codes.names_doc_has_font_size_drift(
{"kind": "names", "names": [{"source": source, "text": shrunk}]}
)
)
if __name__ == "__main__":

View file

@ -137,34 +137,14 @@ class TestNameWrapRoles(unittest.TestCase):
def tearDown(self):
self._tmp.cleanup()
def test_roles_path_beside_names(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):
def test_upsert_load_and_update_name_wrap_role(self):
roles_path = wn.roles_json_path_for_names(self.names)
wn.upsert_name_wrap_role(roles_path, "説明", width=48, max_lines=3)
roles = wn.load_name_wrap_roles(roles_path)
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)
self.assertEqual(roles_path, self.dir / "wolfdawn-roles.json")
wn.upsert_name_wrap_role(roles_path, "説明", width=40, font=14)
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), "説明")
self.assertEqual(role["width"], 52)
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)

View file

@ -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):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
@ -81,172 +66,29 @@ class TestWrapSearch(unittest.TestCase):
def tearDown(self):
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)
self.assertEqual(len(hits), 1)
hit = hits[0]
self.assertEqual(hit.kind, "db")
self.assertEqual(hit.sheet_name, RUMOR_SHEET)
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"
doc = json.loads(path.read_text(encoding="utf-8"))
changed = ws.wrap_overflow_in_sheet(path, doc, RUMOR_SHEET, width=34)
self.assertEqual(changed, 1)
doc2 = json.loads(path.read_text(encoding="utf-8"))
line = ws.locate_line(
doc2,
{
"kind": "db",
"sheet_name": RUMOR_SHEET,
"row": GATEKEEPER_ROW,
"field_name": GATEKEEPER_FIELD,
},
)
self.assertIsNotNone(line)
line = ws.locate_line(doc, hit.hit_id)
assert line is not None
self.assertIn("\n", line["text"])
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")
profile = ws.load_wrap_profile(self.work)
self.assertEqual(ws.get_sheet_width(profile, 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",
self.assertEqual(
ws.get_sheet_width(ws.load_wrap_profile(self.work), RUMOR_SHEET), 34
)
def tearDown(self):
self.tmp.cleanup()
def test_search_finds_names_json_profile_text(self):
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):
class TestNamesSearchAndWrap(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.translated = Path(self.tmp.name) / "translated"
@ -254,11 +96,10 @@ class TestNamesCategoryWrap(unittest.TestCase):
self.category = "├■街の噂MOB"
fixture = {
"kind": "names",
"count": 2,
"names": [
{
"source": "短い",
"text": "Short line.",
"source": "「メイドがどんどん辞めていく」",
"text": '"Yeah, maids keep quitting one after another."',
"note": self.category,
},
{
@ -277,107 +118,44 @@ class TestNamesCategoryWrap(unittest.TestCase):
def tearDown(self):
self.tmp.cleanup()
def test_names_categories_in_sheet_summaries(self):
summaries = ws.sheet_overflow_summaries(self.translated, width=34)
names_rows = [s for s in summaries if s.kind == "names" and s.sheet_name == self.category]
self.assertEqual(len(names_rows), 1)
self.assertEqual(names_rows[0].line_count, 2)
self.assertEqual(names_rows[0].overflow_count, 1)
def test_search_summary_shows_translated_text(self):
hits = ws.search_translated_text("maids", self.translated)
self.assertEqual(len(hits), 1)
summary = hits[0].summary(40)
self.assertTrue(summary.split("\n", 1)[0].startswith('"Yeah, maids'))
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"))
changed = ws.wrap_overflow_in_sheet(
self.path, doc, self.category, 34, kind="names",
)
self.assertEqual(changed, 1)
doc2 = json.loads(self.path.read_text(encoding="utf-8"))
long_entry = doc2["names"][1]["text"]
self.assertIn("\n", long_entry)
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'))
self.assertGreaterEqual(changed, 1)
long_text = json.loads(self.path.read_text(encoding="utf-8"))["names"][1]["text"]
self.assertIn("\n", long_text)
self.assertLessEqual(dazedwrap.max_line_visible_length(long_text), 34)
class TestEventScopeWrap(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.translated = Path(self.tmp.name) / "translated"
self.translated.mkdir()
self.path = self.translated / "Map001.mps.json"
self.path.write_text(
json.dumps(
{
"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."},
],
}
],
},
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 TestWrapPreviewHtml(unittest.TestCase):
def test_html_preview_hides_codes_and_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)
self.assertNotIn(r"\c[", html)
self.assertNotIn(r"\f[", html)
self.assertIn("cafe", html)
self.assertIn("font-size:14px", html) # 20*13/18
self.assertIn("font-size:13px", html)
self.assertIn(ws.wolf_preview_color(21), html)
info = ws.wrap_preview_info(text, 40)
self.assertLess(info["longest"], len(text))
class TestManualFontAndWrap(unittest.TestCase):
def test_scales_emphasis_then_wraps(self):
def test_manual_scales_emphasis_wraps_and_skips_short_plain(self):
text = (
r"So they even take worries like this in the "
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.assertLessEqual(dazedwrap.max_line_visible_length(new_text), 40)
def test_group_manual_skips_short_plain_lines(self):
text = "Short."
new_text, changed = ws.apply_manual_font_and_wrap(
text, 40, font=14, only_overflow_or_fonts=True
short, skipped = ws.apply_manual_font_and_wrap(
"Short.", 40, font=14, only_overflow_or_fonts=True
)
self.assertFalse(changed)
self.assertEqual(new_text, text)
self.assertFalse(skipped)
self.assertEqual(short, "Short.")
def test_wrap_overflow_manual_in_names_category(self):
tmp = tempfile.TemporaryDirectory()
try:
path = Path(tmp.name) / "names.json"
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "names.json"
doc = {
"kind": "names",
"names": [
@ -428,12 +203,48 @@ class TestManualFontAndWrap(unittest.TestCase):
changed = ws.wrap_overflow_manual_in_scope(path, doc, hit, 36, font=14)
self.assertEqual(changed, 1)
saved = json.loads(path.read_text(encoding="utf-8"))
out = saved["names"][0]["text"]
self.assertIn(r"\f[14]", out)
self.assertIn(r"\f[14]", saved["names"][0]["text"])
self.assertEqual(saved["names"][1]["text"], "OK")
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__":

View file

@ -1,5 +1,5 @@
#!/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
@ -17,7 +17,7 @@ from util import wolfdawn # noqa: E402
class TestRelayoutWrappers(unittest.TestCase):
def test_relayout_dry_run_args(self):
def test_relayout_auto_metrics(self):
captured = {}
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])
with patch.object(wolfdawn, "_run", side_effect=fake_run):
wolfdawn.relayout("/game/Data", width=55, max_rows=0)
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,
)
wolfdawn.relayout("/game/Data", width="auto", max_rows=0, out_dir="/tmp/out")
args = captured["args"]
self.assertEqual(args[0], "relayout")
self.assertEqual(args[:2], ["relayout", "/game/Data"])
self.assertIn("-o", args)
self.assertIn("/tmp/out", args)
self.assertIn("--width", args)
self.assertIn("60", args)
self.assertIn("auto", args)
self.assertIn("--max-rows", args)
self.assertIn("4", args)
self.assertIn("--no-resolve", args)
# max_rows=0 becomes auto
self.assertEqual(args[args.index("--max-rows") + 1], "auto")
def test_desc_relayout_args(self):
def test_desc_relayout_auto_max_lines(self):
captured = {}
def fake_run(args, log_fn=None):
@ -75,88 +53,32 @@ class TestRelayoutWrappers(unittest.TestCase):
)
args = captured["args"]
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("auto", args)
self.assertEqual(args[args.index("--max-lines") + 1], "auto")
self.assertIn("--keep-breaks", args)
def test_relayout_auto_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.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):
def test_names_wrap_flags_and_parsers(self):
captured = {}
def fake_run(args, log_fn=None):
captured["args"] = list(args)
return wolfdawn.WolfResult(
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],
)
with patch.object(wolfdawn, "_run", side_effect=fake_run):
res = wolfdawn.names_wrap("/tmp/names.json")
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(
res = wolfdawn.names_wrap(
"/tmp/names.json",
note="├■街の噂MOB",
width=66,
note="説明",
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(
captured["args"],
[
@ -168,16 +90,11 @@ class TestRelayoutWrappers(unittest.TestCase):
"3",
"--note",
"説明",
"--dry-run",
],
)
def test_parse_names_wrap_shrink_count(self):
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)
self.assertEqual(wolfdawn.parse_names_wrap_counts(res.stdout), 7)
self.assertEqual(wolfdawn.parse_names_wrap_shrink_count(res.stdout), 2)
if __name__ == "__main__":

View file

@ -134,27 +134,6 @@ def scale_font_sizes(
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]:
"""Return the sequence of WolfDawn-compared control codes in *text*."""
if not isinstance(text, str) or not text:

View file

@ -9,12 +9,11 @@ from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence
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
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:
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:
"""One-line status for the preview header."""
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:
"""Return the live line dict for *hit_id* inside *doc*."""
kind = hit_id.get("kind")
@ -1081,96 +1041,3 @@ def _wrap_overflow_in_names_category(
if changed:
save_document(path, doc)
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