JJ_MKVMaker/tool/l10n/codemod.py

268 lines
8.0 KiB
Python

"""한글 문자열을 tr('…') / trf('…{0}…', [x]) 로 감싸는 코드 변환.
- 원시 문자열 (r'…') 은 건드리지 않는다 (정규식 등).
- 붙어 있는 문자열 ('a' 'b') 은 하나로 묶는다.
- 보간 ($x, ${expr}) 은 {0}, {1} … 로 바꾸고 인자 목록으로 넘긴다 (식 안의 문자열도 다시 변환).
- 이미 tr( / trf( 안에 있는 문자열은 건드리지 않는다.
"""
import os
import re
import sys
HANGUL = re.compile('[가-힣]')
IDENT = re.compile(r'[A-Za-z_][A-Za-z0-9_]*')
class Lit:
def __init__(self, raw, quote, parts, src):
self.raw = raw # r'' 여부
self.quote = quote # ' " ''' """
self.parts = parts # [('text', src_text) | ('expr', expr_src, is_brace)]
self.src = src # 원래 소스
def skip_ws_comments(s, i):
n = len(s)
while i < n:
if s[i].isspace():
i += 1
elif s.startswith('//', i):
j = s.find('\n', i)
i = n if j < 0 else j + 1
elif s.startswith('/*', i):
i = skip_block_comment(s, i)
else:
break
return i
def skip_block_comment(s, i):
depth = 0
n = len(s)
while i < n:
if s.startswith('/*', i):
depth += 1
i += 2
elif s.startswith('*/', i):
depth -= 1
i += 2
if depth == 0:
return i
else:
i += 1
return n
def string_start(s, i):
"""i 에서 문자열이 시작하면 (raw, quote, 본문 시작) 아니면 None"""
raw = False
j = i
if s[j] in 'rR' and j + 1 < len(s) and s[j + 1] in '\'"':
if j > 0 and (s[j - 1].isalnum() or s[j - 1] in '_$'):
return None
raw = True
j += 1
if j >= len(s) or s[j] not in '\'"':
return None
q = s[j]
if s.startswith(q * 3, j):
return raw, q * 3, j + 3
return raw, q, j + 1
def parse_string(s, i):
"""i 는 문자열 시작 (r 포함). (Lit, 끝 위치)"""
raw, quote, j = string_start(s, i)
parts = []
buf = []
n = len(s)
while j < n:
if s.startswith(quote, j):
if buf:
parts.append(('text', ''.join(buf)))
j += len(quote)
return Lit(raw, quote, parts, s[i:j]), j
c = s[j]
if c == '\\' and not raw:
buf.append(s[j:j + 2])
j += 2
continue
if c == '$' and not raw:
if j + 1 < n and s[j + 1] == '{':
end = match_brace(s, j + 1)
if buf:
parts.append(('text', ''.join(buf)))
buf = []
parts.append(('expr', s[j + 2:end], True))
j = end + 1
continue
m = IDENT.match(s, j + 1)
if m:
if buf:
parts.append(('text', ''.join(buf)))
buf = []
parts.append(('expr', m.group(0), False))
j = m.end()
continue
buf.append(c)
j += 1
raise ValueError('끝나지 않은 문자열 at %d' % i)
def match_brace(s, i):
"""s[i] == '{' 에 맞는 '}' 위치 (안의 문자열 · 주석 고려)"""
depth = 0
n = len(s)
j = i
while j < n:
c = s[j]
if s.startswith('//', j) or s.startswith('/*', j):
j = skip_ws_comments(s, j)
continue
st = string_start(s, j)
if st:
_, j = parse_string(s, j)
continue
if c == '{':
depth += 1
elif c == '}':
depth -= 1
if depth == 0:
return j
j += 1
raise ValueError('괄호가 맞지 않음')
def has_hangul_text(lits):
return any(p[0] == 'text' and HANGUL.search(p[1]) for l in lits for p in l.parts)
def emit_group(lits, transform):
"""붙어 있는 문자열 묶음 → tr(...) / trf(..., [...])"""
args = []
pieces = []
for l in lits:
body = []
for p in l.parts:
if p[0] == 'text':
# 템플릿 자리 표시와 헷갈리지 않게 원래 {n} 은 그대로 둔다 (드묾)
body.append(p[1])
else:
body.append('{%d}' % len(args))
args.append(transform(p[1]).strip())
pieces.append(l.quote + ''.join(body) + l.quote)
lit = ' '.join(pieces)
if args:
return 'trf(%s, [%s])' % (lit, ', '.join(args))
return 'tr(%s)' % lit
def transform(s):
out = []
i = 0
n = len(s)
while i < n:
c = s[i]
if s.startswith('//', i):
j = s.find('\n', i)
j = n if j < 0 else j
out.append(s[i:j])
i = j
continue
if s.startswith('/*', i):
j = skip_block_comment(s, i)
out.append(s[i:j])
i = j
continue
st = string_start(s, i)
if st and not (c in 'rR' and i > 0 and (s[i - 1].isalnum() or s[i - 1] in '_$')):
# 붙어 있는 문자열 모으기
start = i
lits = []
lit, j = parse_string(s, i)
lits.append(lit)
end = j
while True:
k = skip_ws_comments(s, end)
if k < n and string_start(s, k):
lit, j2 = parse_string(s, k)
lits.append(lit)
end = j2
else:
break
already = re.search(r'\btrf?\(\s*$', ''.join(out)[-20:]) is not None
directive = re.search(r'(^|\n)\s*(import|export|part|library)\b[^;\n]*$', ''.join(out)[-200:]) is not None
if (not already and not directive and has_hangul_text(lits)
and not any(l.raw for l in lits)):
out.append(emit_group(lits, transform))
else:
# 그대로 두되, 보간 식 안의 문자열은 변환
out.append(rebuild_with_exprs(s[start:end], transform))
i = end
continue
out.append(c)
i += 1
return ''.join(out)
def rebuild_with_exprs(src, transform):
"""문자열 (묶음) 원문에서 ${...} 안만 변환"""
out = []
i = 0
n = len(src)
while i < n:
st = string_start(src, i)
if st:
raw, quote, j = st
out.append(src[i:j])
while not src.startswith(quote, j):
ch = src[j]
if ch == '\\' and not raw:
out.append(src[j:j + 2])
j += 2
elif ch == '$' and not raw and j + 1 < n and src[j + 1] == '{':
e = match_brace(src, j + 1)
out.append('${' + transform(src[j + 2:e]) + '}')
j = e + 1
else:
out.append(ch)
j += 1
out.append(quote)
i = j + len(quote)
else:
out.append(src[i])
i += 1
return ''.join(out)
def add_import(src, path):
rel = os.path.relpath(os.path.join('lib', 'l10n', 'tr.dart'), os.path.dirname(path)).replace('\\', '/')
line = "import '%s';\n" % rel
if line in src:
return src
imports = list(re.finditer(r"^import '[^']+';\n", src, re.M))
if imports:
last = imports[-1]
return src[:last.end()] + line + src[last.end():]
return line + src
if __name__ == '__main__':
skip = {os.path.normpath(p) for p in sys.argv[2:]}
changed = 0
for root, _, files in os.walk(sys.argv[1]):
for f in files:
if not f.endswith('.dart'):
continue
path = os.path.join(root, f)
if os.path.normpath(path) in skip:
continue
src = open(path, encoding='utf-8').read()
new = transform(src)
if new != src:
new = add_import(new, path)
open(path, 'w', encoding='utf-8', newline='').write(new)
changed += 1
print('변환', path)
print('파일', changed)