렉시컬 규칙
이 문서는 템플릿 소스를 텍스트, 태그, 주석으로 나누는 방법과 그 과정에서 제거하는 공백을 정의한다. 태그 본문은 grammar.md가 정의한다. 표현식 토큰은 expressions.md가 정의한다. 오류 객체와 위치는 errors.md가 정의한다.
1. 소스
LEX-1 템플릿 소스는 UTF-8로 인코딩된 바이트 열이다. 유효한 UTF-8이 아닌 바이트 열은 E_LEX_INVALID_UTF8로 거부한다. 보고하는 위치는 첫 번째 잘못된 바이트다.
LEX-2 소스 시작의 바이트 순서 표식(EF BB BF)은 다른 처리보다 먼저 제거한다. 표식은 텍스트가 아니다. 위치와 AST span의 바이트 오프셋은 표식 다음 첫 바이트부터 센다.
LEX-3 줄 구분자는 \n과 \r\n이다. \r\n 쌍은 하나의 구분자다. 텍스트 안의 구분자는 쓰인 그대로 출력에 유지한다. 줄 번호는 구분자를 1부터 센다.
LEX-4 소스는 텍스트 조각, 태그, 주석의 열이다. 태그 밖이면서 주석 밖인 모든 바이트는 텍스트다. 텍스트는 LEX-14가 정의하는 제거와 LEX-9가 정의하는 이스케이프를 제외하고 변경 없이 출력한다.
2. 태그 시작
LEX-5 태그는 기호 형태의 {에서 시작한다: {, 그 뒤 0개 이상의 수평 공백 문자(공백 U+0020 또는 탭 U+0009), 그 뒤 기호. 기호는 =, @, ?#, ?, :?, :, /, +, #, *, %다. 기호는 가장 긴 형태로 일치시킨다: ?#을 ?보다 먼저, :?를 :보다 먼저 일치시킨다. 두 기호는 추가 문맥이 필요하다: /는 뒤에 0개 이상의 수평 공백 문자와 닫는 구분자가 올 때만 태그를 시작하고, @는 뒤에 0개 이상의 수평 공백 문자, 식별자, 0개 이상의 수평 공백 문자, =가 올 때만 태그를 시작한다. 그 문맥이 없는 /나 @의 {는 텍스트다. 이유: GRM-5는 기호와 본문 사이의 공백을 선택으로 정하고 모든 파서가 {@item = items}와 {@item=items}를 받는다. 식별자와 =만으로도 { @media (max-width: 600px) { 같은 CSS at-rule은 공백 요구 없이 텍스트로 남는다.
LEX-6 태그는 { 뒤에 0개 이상의 수평 공백 문자, :, 식별자가 올 때만 대입 형태로 시작한다. : 뒤에는 선택적 수평 공백을 둘 수 있다. 식별자([A-Za-z_][A-Za-z0-9_]*) 뒤에는 0개 이상의 수평 공백과 대입 연산자가 온다. 대입 연산자는 = 또는 >가 뒤따르지 않는 =, +=, -=, *=, /=, %=, ++, --다. 예약어(true, false, null, in)는 식별자 패턴과 일치하므로 태그가 시작되고, 그 뒤 grammar.md가 대입을 거부한다. 식별자가 { 바로 뒤에 오는 {a = 1}은 텍스트다. 이유: 대입 형태의 :는 LEX-5의 : 기호이며 LEX-5는 모든 기호 형태에서 { 뒤의 수평 공백을 허용하고, 모든 파서가 { :x = 1}을 받는다. { x = 1 }은 기호가 없으므로 텍스트로 남는다.
LEX-7 LEX-5와 LEX-6 중 어느 것도 만족하지 않는 {는 텍스트다. {와 기호 사이의 줄 구분자는 기호 형태를 막는다. 다음 중괄호는 텍스트다:
{ debug: true }
{}
${name}
{a:1}
{
color: red;
}
{ x = 1 }
function f() { return 1; }
{/* comment */}
{/re/.test(s)}
.a { @media (max-width: 600px) { color: red } }다음 중괄호는 태그를 시작한다:
{= title}
{=title}
{ = title }
{@ item = items}
{? item.index_ == 0}
{:? item.last_}
{:}
{/}
{ /}
{+ parts/head.tpl}
{# footer.tpl year}
{?# contents}
{* note *}
{:count = 0}
{ :count = 0}
{:count += 1}
{:count++}
{@item = items}다음 중괄호는 태그를 시작하지만 본문이 오류다. \{ 이스케이프(LEX-9)로 텍스트로 남길 수 있다:
class A { #count = 0 }
{= }LEX-8 태그가 시작된 뒤에는 태그 본문이 grammar.md를 만족해야 한다. 문법을 만족하지 않는 본문은 오류다. 태그를 텍스트로 다시 해석하지 않는다.
3. 이스케이프
LEX-9 열 \{는 그 {가 LEX-5 또는 LEX-6에 의해 태그를 시작할 때만 이스케이프다. 이스케이프는 {를 출력하고 뒤따르는 문자는 텍스트다. 그 외 모든 위치에서 \{는 두 개의 텍스트 문자다. 텍스트에 다른 이스케이프는 없다.
\{= title}는 다음을 출력한다.
{= title}열 \\{/}는 \{/}를 출력한다: 첫 \는 텍스트이고, {/}가 닫기 태그를 시작할 것이므로 \{는 이스케이프다.
4. 태그 종료
LEX-10 기호가 *인 기호 형태의 태그는 주석이다. 주석은 여는 * 뒤 첫 *}에서 끝난다. 그 사이의 내용은 해석하지 않고 출력을 만들지 않는다. 주석은 문장 노드를 갖지 않으며, AST는 주석을 Template.comments에 나열한다(AST-9). 주석은 중첩하지 않는다. 소스 끝 전에 끝나지 않는 주석은 E_PARSE_UNTERMINATED_COMMENT로 거부한다.
{* removed *}
{**}
{*
multi-line
*}LEX-11 그 외 모든 태그는 문자열 리터럴 밖에 있고 표현식 괄호 깊이가 0이며 본문 문법이 태그 경계로 받아들이는 첫 닫는 구분자에서 끝난다. 기본 구분자에서는 어떤 문법 규칙도 }를 받아들이지 않으므로 태그는 문자열 리터럴 밖의 첫 }에서 끝난다. 문법이 쓰는 문자, 예를 들어 ]가 닫는 구분자이면 열린 [를 닫는 ]는 본문에 속하고 다음 ]가 태그를 끝낸다: [= a[0]]는 하나의 echo 태그다. 문자열 리터럴은 ' 또는 "에서 시작하고 이스케이프되지 않은 같은 따옴표 문자에서 끝난다. 문자열 리터럴의 이스케이프 규칙은 expressions.md가 정의한다. 본문이 닫는 } 없이 소스 끝에 도달한 태그는 E_PARSE_UNTERMINATED_TAG로 거부한다. 소스 끝에 도달한 문자열 리터럴은 E_PARSE_UNTERMINATED_STRING으로 거부한다.
{= "}"}
{= 'a' + "b"}LEX-12 수평 공백은 기호와 본문 사이, expressions.md가 허용하는 본문 안, 닫는 } 앞에 허용한다. 태그 { /}와 태그 {/ }는 둘 다 닫기 태그다.
LEX-13 기호가 =인 태그는 echo 태그다. 주석을 포함한 그 외 모든 태그는 non-echo 태그다.
5. Standalone 줄
LEX-14 줄 그룹은 그룹의 줄에서 시작하는 모든 태그나 주석이 그룹의 줄에서 끝나는 가장 작은 연속 소스 줄의 집합이다. 태그가 없는 줄은 한 줄짜리 줄 그룹이다. 줄 그룹은 다음을 모두 만족할 때 standalone이다:
- 그룹이 태그나 주석을 하나 이상 포함한다;
- 그룹이 echo 태그를 포함하지 않는다;
- 태그와 주석 밖에 있는 그룹의 모든 바이트가 공백, 탭, 줄 구분자 중 하나다.
standalone 줄 그룹에서는 태그와 주석 밖의 모든 바이트를 출력에서 제거하며, 그룹의 마지막 줄을 끝내는 줄 구분자도 제거한다. 그룹의 태그는 평소대로 처리한다. 그룹 앞 줄을 끝내는 줄 구분자는 유지한다.
<ul>
{@ item = items}
<li>{= item}</li>
{/}
</ul>는 items가 ["a", "b"]일 때 다음을 출력한다.
<ul>
<li>a</li>
<li>b</li>
</ul>echo 태그는 줄을 유지한다:
<p>
{= title}
</p>는 다음을 출력한다.
<p>
Title
</p>한 줄의 두 태그는 하나의 standalone 그룹을 이룬다:
{@ a = x}{@ b = y}
{= b}
{/}{/}여러 줄에 걸친 태그는 그것이 차지하는 모든 줄과 함께 그룹을 이룬다:
{*
note
*}
text는 다음을 출력한다.
textLEX-15 standalone이 아닌 줄의 태그나 주석은 그 출력으로 치환하고, 줄의 다른 모든 바이트는 유지한다.
<span>{? on}on{:}off{/}</span>는 on이 true일 때 다음을 출력한다.
<span>on</span>6. 위치
LEX-16 위치는 LEX-2의 제거 이후 소스에 대한 바이트 오프셋이다. span은 바이트 오프셋 쌍 [start, end)다. 텍스트 노드는 LEX-14의 제거 이전에 그것이 만들어진 바이트를 가리킨다. 위치의 줄과 열은 errors.md가 정의한다.
7. 래퍼 태그
LEX-17 래퍼 태그는 본문이 두 번 쓴 여는 구분자와 두 번 쓴 닫는 구분자로 둘러싸이고, 그 전체가 래퍼 쌍으로 둘러싸인 태그다. 래퍼 쌍은 "와 ", '와 ', /*와 */, <!--와 -->다. 래퍼 태그의 형태는 래퍼 열기, 선택적 수평 공백, {{, 태그 본문, }}, 선택적 수평 공백, 래퍼 닫기다. 태그 본문은 LEX-5, LEX-6, grammar.md에 따른 태그 본문이며, 두 번째 {가 LEX-5와 LEX-6의 { 역할을 한다. 래퍼를 포함한 래퍼 태그 전체가 태그의 출력으로 치환된다. 래퍼는 텍스트가 아니다.
var page = "{{= json(page)}}";
/* {{= theme.css}} */
<!-- {{# content}} -->page가 {"id": 7}일 때 첫 줄은 다음을 출력한다:
var page = {"id":7};LEX-18 {{는 래퍼 열기가 수평 공백만 사이에 두고 앞에 있고 두 번째 {가 LEX-5 또는 LEX-6을 만족할 때만 래퍼 태그를 시작한다. 그 외 모든 위치에서 {{의 첫 {는 LEX-7에 따라 텍스트이고 두 번째 {는 따로 판정한다.
{{ msg }}
"{{ msg }}"
"{= name}"첫 줄은 텍스트다: 첫 { 뒤에 {가 오고, 두 번째 { 뒤에 공백과 m이 온다. 둘째 줄도 같은 이유로 텍스트다. 셋째 줄은 따옴표 문자열 안의 echo 태그다. 따옴표는 텍스트이며 출력에 남는다.
LEX-19 래퍼 태그의 본문은 두 번 쓴 닫는 구분자를 하나의 닫는 단위로 읽었을 때 LEX-11이 본문에 대해 선택하는 두 번 쓴 닫는 구분자에서 끝난다. 주석은 첫 *}}에서 끝난다. }} 뒤에는 선택적 수평 공백과 래퍼 열기에 대응하는 래퍼 닫기가 와야 한다. 본문이 소스 끝에 도달하는 래퍼 태그는 E_PARSE_UNTERMINATED_TAG으로 거부한다. }} 뒤에 래퍼 닫기가 오지 않는 래퍼 태그는 래퍼 열기 위치에서 E_PARSE_INVALID_WRAPPER로 거부한다. LEX-14에서 래퍼 태그는 하나의 태그이고 래퍼 바이트는 그 태그에 속한다.
8. 구분자
LEX-20 여는 구분자와 닫는 구분자는 엔진 옵션이나 지시문이 다른 문자를 선택하지 않는 한 {와 }다. 이 문서와 grammar.md에서 {를 지칭하는 모든 규칙은 여는 구분자에, }를 지칭하는 모든 규칙은 닫는 구분자에 적용된다. LEX-17의 두 번 쓴 형태는 여는 구분자를 두 번, 닫는 구분자를 두 번 쓴 것이다. LEX-9의 이스케이프는 \ 뒤에 여는 구분자를 쓴 것이다.
LEX-21 구분자는 글자, 숫자, _, \, 공백, 제어 문자가 아닌 ASCII 문자 하나다. 여는 구분자와 닫는 구분자는 같은 문자여도 된다. 표현식 문법이 연산자나 괄호로 쓰는 닫는 구분자는 완전한 표현식 뒤의 괄호 깊이 0에서 태그 경계가 되고, 문자열 리터럴 안이나 중첩 괄호 안에서는 표현식의 일부로 남는다. 따라서 유효한 모든 구분자 쌍을 사용할 수 있고 표현식 구조도 보존된다.
LEX-22 엔진 옵션 delimiters는 엔진이 파싱하는 모든 템플릿의 구분자를 선택한다. 값은 두 문자로 된 문자열이다: 여는 구분자 뒤에 닫는 구분자. 기본값은 {}다.
LEX-23 지시문 태그는 그 태그를 포함하는 파일의 나머지 부분에 대한 구분자를 선택한다. 본문은 기호 %, 단어 delimiter, 두 문자로 된 문자열이다. 지시문은 그 앞에 유효한 구분자로 쓴다. 지시문은 파일의 첫 태그로서 앞에 공백만 있을 때만 유효하다. 다른 위치의 지시문은 E_PARSE_INVALID_DIRECTIVE로 거부한다. 문자가 LEX-21을 만족하지 않거나 본문이 다른 형태인 지시문은 E_PARSE_INVALID_DIRECTIVE로 거부한다. 지시문은 echo가 아닌 태그이며, 출력을 만들지 않고 AST에 노드가 없다.
{% delimiter ;;}
<p>;= title;</p>
;@ item = items;
<li>;= item;</li>
;/;
var page = ";;= json(page);;";title이 T, items가 ["a"], page가 {"id": 7}일 때 다음을 출력한다
<p>T</p>
<li>a</li>
var page = {"id":7};