UniText 의 태그는 등록한 것만 읽힌다
uniText.Text = "<b>굵게</b>"; // 화면에 <b>굵게</b> 가 그대로 나온다
UniText 에는 기본으로 켜진 태그가 없습니다. 에러도 경고도 없이 < 가 평범한 글자로 찍힙니다.
UniText 의 마크업은 “무엇을 찾을지”(파싱 규칙)와 “찾은 범위에 무엇을 할지”(모디파이어)를 한 쌍으로 등록한 것만 태그로 읽습니다. 읽힌 태그는 셰이핑 전에 지워지고, 효과는 코드포인트 번호에 적어 두었다가 메시를 만들 때 붙습니다. 기준은 1.0.0 브랜치의 커밋 2087eff 입니다.
규칙과 모디파이어를 짝지어 등록한다
| 조각 | 계약 | 하는 일 |
|---|---|---|
| 파싱 규칙 | IParseRule (태그형은 TagParseRule) |
원문에서 패턴을 찾아 범위와 파라미터를 만든다 |
| 모디파이어 | BaseModifier (글리프 효과는 GlyphModifier<T>) |
그 범위에 효과를 기록하고 메시 때 정점에 입힌다 |
둘을 나눈 덕에 문법만 갈아 끼울 수 있습니다. <b>, 직접 만든 마크다운 규칙의 **굵게**, BBCode 규칙의 [b] 가 모두 같은 BoldModifier 를 씁니다. 파서는 등록된 규칙에게만 물으니 쓰지 않는 태그의 파싱 비용도 없습니다. 대가는 등록을 빠뜨리면 마크업이 전부 평문이 된다는 것입니다.
내장 태그는 <b>, <i>, <u>, <s>, <upper>, <color>, <size>, <gradient>, <cspace>, <line-height>, <line-spacing>, <ellipsis>, <link>, <obj/> 이고, 등록은 인스펙터의 Mod Registers, 코드의 RegisterModifier, 여러 컴포넌트가 나눠 쓰는 ModRegisterConfig 에셋 셋 중 하나로 합니다.
태그는 셰이핑 전에 지워지고 번호가 옮겨진다
캔버스가 그리기 직전, 첫 패스보다 먼저 AttributeParser 가 태그를 찾아 지웁니다. 셰이핑은 태그가 있었다는 것을 모릅니다. 문제는 번호입니다. 규칙이 찾은 범위는 원문 번호인데, 효과는 태그가 빠진 텍스트에 걸어야 합니다. 파서는 원문을 한 번 훑으며 원문 번호에서 새 번호로 가는 표를 만들고, 지운 글자에는 -1 을 적습니다.
// offset: 지금까지 지운 글자 수
for (var i = 0; i <= text.Length; i++)
{
if (i >= removal.start && i < removal.end)
{
// 태그 글자다. 버리고 -1 을 적는다
offset++;
indexMap[i] = -1;
}
else
{
// 남는 글자다. 지운 수만큼 앞으로 당긴 번호를 적는다
indexMap[i] = i - offset;
clean += text[i];
}
}
RemapSpanIndices(spans, indexMap); // 규칙이 찾은 범위를 새 번호로 옮긴다
a<b>b</b>c 를 넣으면 표가 이렇게 채워집니다.
| 원문 번호 | 0 | 1~3 | 4 | 5~8 | 9 | 10 |
|---|---|---|---|---|---|---|
| 글자 | a |
<b> |
b |
</b> |
c |
끝 |
| 새 번호 | 0 | -1 | 1 | -1 | 2 | 3 |
- 태그를 걷어 낸 텍스트는
abc입니다. - 굵게 칠할 범위는 원문 번호로
[4, 5)였고, 표로 옮기면[1, 2)입니다.abc의 가운데b가 정확히 걸립니다. - 표의 칸 수는 원문 길이 + 1 입니다. 문자열 끝 위치(10)도 옮겨 적어야 범위의 끝을 바꿀 수 있기 때문입니다.
번호 변환은 한 번 더 있습니다. C# 의 string 은 글자를 16비트 char 로 담는데, 이모지처럼 번호가 큰 글자는 char 둘을 써서 하나를 적습니다(서로게이트 쌍). 이 표는 char 번호이고 파이프라인 뒤쪽은 글자 하나에 번호 하나인 코드포인트 번호를 쓰므로, 두 char 를 한 번호로 접는 두 번째 표를 거칩니다. 모디파이어가 받는 범위는 태그를 걷어 낸 텍스트의 코드포인트 번호입니다. 닫히지 않은 태그는 텍스트 끝을 닫는 위치로 잡힙니다.
효과는 번호에 적어 두었다가 메시 때 붙는다
모디파이어의 Apply 는 바로 그리지 않고 코드포인트마다 한 칸인 버퍼에 값을 적어 둡니다. 정점에 입히는 것은 한참 뒤 메시를 만들 때입니다. 셰이핑을 거치면 글자 수와 글리프 수가 달라지기 때문에, 글리프마다 “이 글리프는 원문의 몇 번째 글자에서 나왔다” 는 번호가 붙어 있습니다. 이 번호를 cluster 라고 부르고, 메시 생성기는 이 번호로 효과를 적어 둔 칸을 찾습니다.
그래서 합자로 글자 둘이 글리프 하나가 되든 방향 분석이 순서를 뒤집든 효과가 제자리를 찾습니다. 순서도 정해져 있습니다. 범위는 닫는 태그를 만날 때 목록에 쌓이고 Apply 는 역순으로 돌기 때문에, <color=red>a<color=blue>b</color>c</color> 에서 b 는 파랑, a 와 c 는 빨강입니다. 중첩한 같은 태그는 안쪽이 이깁니다.
주의할 점
1. 쓰는 태그를 전부 등록했는지 번역 문자열로 확인한다
안 지키면 생기는 문제. 등록하지 않은 태그는 에러 없이 글자로 나옵니다. 번역 쪽에서 <size> 를 쓰기 시작하면 그 언어 화면에만 <size=120%> 가 찍힙니다. 모디파이어 인스턴스 하나를 두 컴포넌트에 RegisterModifier 하면 소유권 검사에 걸려 Debug.LogError 가 납니다.
이렇게 쓴다. 쓰는 규칙과 모디파이어를 ModRegisterConfig 에셋 하나에 모아 프리팹마다 넣습니다. 에셋은 컴포넌트마다 복제되어 등록되므로 여러 컴포넌트가 같이 써도 됩니다(에디터에서 두 컴포넌트가 각자 제 색으로 그려지고 에러 0 건).
2. 닫는 태그를 빠뜨리지 않는다
안 지키면 생기는 문제. 닫히지 않은 태그는 텍스트 끝까지 적용됩니다. 닫는 태그가 빠진 번역 한 줄이 문장 전체를 빨갛게 만들고, 화면만 보고는 어느 문자열이 원인인지 알기 어렵습니다.
이렇게 쓴다. 번역 문자열 검사에 여는 태그와 닫는 태그의 짝 맞추기를 넣습니다. 프로그램이 강조를 넣는 경우라면 태그를 문자열로 잇지 말고 RangeRule 로 범위를 지정합니다.
3. 색 태그의 알파로 투명도를 조절하지 않는다
안 지키면 생기는 문제. ColorModifier 는 정점 색의 알파를 컴포넌트 기본색의 알파로 덮어씁니다. <color=#FF000080> 의 80 은 무시되어 불투명하게 나옵니다(컴포넌트 알파 1 에서 255, 0.5 에서 128).
이렇게 쓴다. 부분 투명이 필요하면 그 부분을 별도 UniText 로 나눠 컴포넌트 알파를 조절합니다. 글 전체 페이드는 컴포넌트 알파만 바꾸면 태그 색까지 따라옵니다.
4. 사용자 입력에 AutoOpenUrl 을 켜 두지 않는다
안 지키면 생기는 문제. AutoOpenUrl 이 켜진 LinkModifier 는 링크 값을 스킴 검사 없이 Application.OpenURL 에 넘깁니다. <link=javascript:alert(1)> 도 file:///C:/Windows 도 그대로 넘어갔습니다. 히트 테스트는 글리프 박스 밖이라도 가장 가까운 글리프 중심까지 20 이내면 그 글리프로 쳐서, 링크 옆 빈 곳을 눌러도 열립니다.
이렇게 쓴다. AutoOpenUrl = false 로 두고 LinkClicked 에서 스킴과 도메인을 확인한 뒤 엽니다. 채팅이나 닉네임을 그리는 컴포넌트에는 링크 규칙을 아예 등록하지 않습니다.
정리
UniText 의 태그는 규칙과 모디파이어를 한 쌍으로 등록한 것만 읽힙니다.
- 등록하지 않은 태그는 에러 없이 글자로 나옵니다. 대신 규칙만 바꾸면 태그, 마크다운, BBCode 가 같은 효과를 씁니다.
- 태그는 셰이핑 전에 지워지고, 범위는 표 두 장을 거쳐 태그 없는 텍스트의 코드포인트 번호로 옮겨집니다.
- 효과는 번호 칸에 적어 두었다가 메시 때
cluster로 붙습니다. 중첩 태그는 안쪽이 이기고, 색 태그의 알파는 컴포넌트 알파로 덮입니다.