268 lines
8.0 KiB
Python
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)
|