218 lines
7.3 KiB
Python
218 lines
7.3 KiB
Python
"""Selective word-wrap for WolfDawn translated JSON.
|
|
|
|
Bulk ``wolf relayout`` / ``desc-relayout`` cannot target individual DB rows or
|
|
custom field types (e.g. ``セリフ_メッセージ``, ``好きなもの_コメント``).
|
|
This module wraps ``text`` fields in ``translated/*.project.json`` for checked
|
|
database sheets and field patterns, then the user re-injects only those files.
|
|
|
|
``wolf relayout`` for maps already skips messages that fit; use it for dialogue
|
|
overflow. Use selective JSON wrap for custom DB columns and fine-grained control.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import util.dazedwrap as dazedwrap
|
|
from util.wolfdawn.db_classify import group_key, json_file_from_doc
|
|
|
|
# Field-name presets for the Relayout tab (match ``fieldName`` substrings).
|
|
FIELD_PRESET_DESCRIPTIONS = re.compile(r"説明|Description", re.IGNORECASE)
|
|
FIELD_PRESET_COMMENTS = re.compile(r"コメント")
|
|
FIELD_PRESET_DIALOGUE = re.compile(r"セリフ|台詞|会話")
|
|
FIELD_PRESET_MESSAGES = re.compile(r"メッセージ")
|
|
FIELD_PRESET_ALL = re.compile(r".", re.DOTALL)
|
|
|
|
FIELD_PRESETS: dict[str, re.Pattern[str]] = {
|
|
"descriptions": FIELD_PRESET_DESCRIPTIONS,
|
|
"comments": FIELD_PRESET_COMMENTS,
|
|
"dialogue": FIELD_PRESET_DIALOGUE,
|
|
"messages": FIELD_PRESET_MESSAGES,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class WrapResult:
|
|
files_touched: int = 0
|
|
lines_wrapped: int = 0
|
|
lines_skipped: int = 0
|
|
touched_json: list[str] | None = None
|
|
|
|
def __post_init__(self):
|
|
if self.touched_json is None:
|
|
self.touched_json = []
|
|
|
|
|
|
def combine_field_patterns(enabled: frozenset[str]) -> re.Pattern[str] | None:
|
|
"""Merge enabled preset keys into one regex, or None when nothing selected."""
|
|
if not enabled:
|
|
return None
|
|
parts = []
|
|
for key in enabled:
|
|
pat = FIELD_PRESETS.get(key)
|
|
if pat is not None and key != "all":
|
|
parts.append(f"(?:{pat.pattern})")
|
|
if "all" in enabled:
|
|
return FIELD_PRESET_ALL
|
|
if not parts:
|
|
return None
|
|
return re.compile("|".join(parts), re.IGNORECASE)
|
|
|
|
|
|
def _visible_length(text: str) -> int:
|
|
return dazedwrap._get_visible_length(text)
|
|
|
|
|
|
def line_needs_wrap(text: str, width: int, *, min_visible: int = 0) -> bool:
|
|
"""True when *text* is long enough and exceeds *width* on at least one line."""
|
|
if not isinstance(text, str) or not text.strip() or width <= 0:
|
|
return False
|
|
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
if not lines:
|
|
visible = _visible_length(text)
|
|
if min_visible > 0 and visible < min_visible:
|
|
return False
|
|
return visible > width
|
|
total_visible = max((_visible_length(line) for line in lines), default=0)
|
|
if min_visible > 0 and total_visible < min_visible:
|
|
return False
|
|
return any(_visible_length(line) > width for line in lines)
|
|
|
|
|
|
def wrap_line_text(text: str, width: int) -> str:
|
|
"""Word-wrap one ``text`` value, preserving WOLF inline codes.
|
|
|
|
Width is measured with :func:`util.dazedwrap._get_visible_length`, so
|
|
``\\c[N]``, ``\\f[N]``, and similar codes do not consume box width.
|
|
"""
|
|
if not isinstance(text, str) or not text.strip() or width <= 0:
|
|
return text
|
|
return dazedwrap.wrapText(text, width)
|
|
|
|
|
|
def group_type_indices(doc: dict[str, Any], group_keys: frozenset[str]) -> list[int]:
|
|
"""Return sorted DB ``type`` indices for selected sheet keys."""
|
|
jf = json_file_from_doc(doc)
|
|
indices: set[int] = set()
|
|
for group in doc.get("groups") or []:
|
|
type_name = str(group.get("typeName") or "")
|
|
if group_key(jf, type_name) not in group_keys:
|
|
continue
|
|
try:
|
|
idx = int(group.get("type", -1))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if idx >= 0:
|
|
indices.add(idx)
|
|
return sorted(indices)
|
|
|
|
|
|
def wrap_db_document(
|
|
doc: dict[str, Any],
|
|
*,
|
|
group_keys: frozenset[str],
|
|
field_pattern: re.Pattern[str] | None,
|
|
width: int,
|
|
min_visible: int = 0,
|
|
only_if_overflow: bool = True,
|
|
) -> tuple[int, int]:
|
|
"""Wrap matching lines in one ``kind: db`` document. Returns (wrapped, skipped)."""
|
|
if doc.get("kind") != "db" or not group_keys:
|
|
return 0, 0
|
|
jf = json_file_from_doc(doc)
|
|
wrapped = skipped = 0
|
|
for group in doc.get("groups") or []:
|
|
type_name = str(group.get("typeName") or "")
|
|
if group_key(jf, type_name) not in group_keys:
|
|
continue
|
|
for line in group.get("lines") or []:
|
|
field_name = str(line.get("fieldName") or "")
|
|
if field_pattern is not None and not field_pattern.search(field_name):
|
|
skipped += 1
|
|
continue
|
|
text = line.get("text")
|
|
if not isinstance(text, str) or not text.strip():
|
|
skipped += 1
|
|
continue
|
|
if only_if_overflow and not line_needs_wrap(
|
|
text, width, min_visible=min_visible
|
|
):
|
|
skipped += 1
|
|
continue
|
|
new_text = wrap_line_text(text, width)
|
|
if new_text != text:
|
|
line["text"] = new_text
|
|
wrapped += 1
|
|
else:
|
|
skipped += 1
|
|
return wrapped, skipped
|
|
|
|
|
|
def wrap_db_translated_dir(
|
|
translated_dir: str | Path,
|
|
*,
|
|
group_keys: frozenset[str],
|
|
field_presets: frozenset[str],
|
|
width: int,
|
|
min_visible: int = 0,
|
|
only_if_overflow: bool = True,
|
|
) -> WrapResult:
|
|
"""Apply selective wrap to all ``*.project.json`` under ``translated/``."""
|
|
base = Path(translated_dir)
|
|
result = WrapResult()
|
|
if not base.is_dir() or not group_keys:
|
|
return result
|
|
field_pattern = combine_field_patterns(field_presets)
|
|
if field_pattern is None:
|
|
return result
|
|
|
|
for path in sorted(base.glob("*.project.json")):
|
|
try:
|
|
doc = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
except Exception:
|
|
continue
|
|
if doc.get("kind") != "db":
|
|
continue
|
|
w, s = wrap_db_document(
|
|
doc,
|
|
group_keys=group_keys,
|
|
field_pattern=field_pattern,
|
|
width=width,
|
|
min_visible=min_visible,
|
|
only_if_overflow=only_if_overflow,
|
|
)
|
|
if w:
|
|
path.write_text(
|
|
json.dumps(doc, ensure_ascii=False, indent=4) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
result.files_touched += 1
|
|
result.lines_wrapped += w
|
|
result.touched_json.append(path.name)
|
|
result.lines_skipped += s
|
|
return result
|
|
|
|
|
|
def collect_field_names(translated_dir: str | Path) -> list[str]:
|
|
"""Sorted unique ``fieldName`` values from staged/translated DB JSON."""
|
|
base = Path(translated_dir)
|
|
names: set[str] = set()
|
|
if not base.is_dir():
|
|
return []
|
|
for path in base.glob("*.project.json"):
|
|
try:
|
|
doc = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
except Exception:
|
|
continue
|
|
if doc.get("kind") != "db":
|
|
continue
|
|
for group in doc.get("groups") or []:
|
|
for line in group.get("lines") or []:
|
|
fn = str(line.get("fieldName") or "").strip()
|
|
if fn:
|
|
names.add(fn)
|
|
return sorted(names)
|