스쿠버 다이빙 슈트를 입고 스텐실을 포장하세요. 오늘 템플릿에 대해 알아보겠습니다!
웹 페이지를 렌더링하거나 이메일을 생성하는 대부분의 소프트웨어는 템플릿을 사용하여 텍스트 문서에 가변 데이터를 포함합니다. 문서의 주요 구조는 데이터에 대한 자리 표시자가 있는 정적 템플릿으로 설정되는 경우가 많습니다. 사용자 이름이나 웹 페이지 콘텐츠와 같은 변수 데이터는 페이지를 렌더링하는 동안 자리 표시자를 대체합니다.
템플릿에 대해 알아보기 위해 많은 프로그래밍 언어에서 사용할 수 있는 템플릿 언어인 Mustache의 하위 집합을 구현합니다. 이 에피소드에서는 다양한 템플릿 방식을 조사할 것입니다. 문자열 연결부터 살펴보고 더 복잡한 템플릿을 허용하는 자체 어휘분석기를 작성하게 됩니다.
네이티브 문자열 보간 사용
최소한의 예부터 시작하겠습니다. 우리 애플리케이션에는 프로젝트 이름을 포함하는 환영 메시지가 필요합니다. 이를 수행하는 가장 빠른 방법은 Ruby에 내장된 문자열 보간 기능을 사용하는 것입니다.
name = "Ruby Magic"
template = "Welcome to #{name}"
# => Welcome to Ruby Magic
엄청난! 그것은 가능했습니다. 그러나 템플릿을 여러 번 재사용하거나 사용자가 템플릿을 업데이트하도록 허용하려면 어떻게 해야 합니까?
보간은 즉시 평가됩니다. 우리는 템플릿을 재사용할 수 없으며(예를 들어 루프에서 재정의하지 않는 한) Welcome to #{name}
를 저장할 수 없습니다. 데이터베이스에서 템플릿을 만들고 잠재적으로 위험한 eval
을 사용하지 않고 나중에 채웁니다. 기능.
운 좋게도 Ruby에는 문자열을 삽입하는 다른 방법이 있습니다. Kernel#sprintf
또는 String#%
. 이러한 메서드를 사용하면 템플릿 자체를 변경하지 않고 보간된 문자열을 얻을 수 있습니다. 이런 식으로 동일한 템플릿을 여러 번 재사용할 수 있습니다. 또한 임의의 Ruby 코드 실행을 허용하지 않습니다. 사용해봅시다.
name = "Ruby Magic"
template = "Welcome to %{name}"
sprintf(template, name: name)
# => "Welcome to Ruby Magic"
template % { name: name }
# => "Welcome to Ruby Magic"
Regexp
템플릿 접근 방식
위의 솔루션이 작동하는 동안, 그것은 완벽하지 않으며 우리가 일반적으로 원하는 것보다 더 많은 기능을 노출합니다. 예를 살펴보겠습니다.
name = "Ruby Magic"
template = "Welcome to %d"
sprintf(template, name: name)
# => TypeError (can't convert Hash into Integer)
Kernel#sprintf
둘 다 및 String#%
다른 유형의 데이터를 처리할 수 있는 특수 구문을 허용합니다. 그들 모두가 우리가 전달하는 데이터와 호환되는 것은 아닙니다. 이 예에서 템플릿은 숫자 형식을 지정해야 하지만 해시가 전달되어 TypeError
를 생성합니다. .
하지만 우리 창고에는 더 많은 전력 도구가 있습니다. 정규 표현식을 사용하여 자체 보간을 구현할 수 있습니다. 정규식을 사용하면 Mustache/Handlebar에서 영감을 받은 스타일과 같은 사용자 정의 구문을 정의할 수 있습니다.
name = "Ruby Magic"
template = "Welcome to {{name}}"
assigns = { "name" => name }
template.gsub(/{{(\w+)}}/) { assigns[$1] }
# => Welcome to Ruby Magic
String#gsub
를 사용합니다. 모든 자리 표시자(이중 중괄호 안의 단어)를 assigns
의 값으로 대체 해시시. 해당 값이 없으면 이 메서드는 아무 것도 삽입하지 않고 자리 표시자를 제거합니다.
이와 같이 문자열에서 자리 표시자를 바꾸는 것은 몇 개의 자리 표시자가 있는 문자열에 대해 실행 가능한 솔루션입니다. 하지만 상황이 좀 더 복잡해지면 빠르게 문제에 봉착하게 됩니다.
템플릿에 조건문이 필요하다고 가정해 보겠습니다. 변수의 값에 따라 결과가 달라야 합니다.
Welcome to
{{name}}!
{{#if subscribed}}
Thank you for subscribing to our mailing list.
{{else}}
Please sign up for our mailing list to be notified about new articles!
{{/if}}
Your friends at
{{company_name}}
정규식은 이 사용 사례를 원활하게 처리할 수 없습니다. 충분히 노력하면 여전히 뭔가를 함께 해킹할 수 있지만 이 시점에서 적절한 템플릿 언어를 구축하는 것이 좋습니다.
템플릿 언어 구축
템플릿 언어를 구현하는 것은 다른 프로그래밍 언어를 구현하는 것과 유사합니다. 스크립팅 언어와 마찬가지로 템플릿 언어에는 렉서, 파서 및 인터프리터의 세 가지 구성 요소가 필요합니다. 하나씩 살펴보겠습니다.
렉서
우리가 처리해야 하는 첫 번째 작업은 토큰화 또는 어휘 분석이라고 합니다. 이 과정은 자연어에서 단어 범주를 식별하는 것과 매우 유사합니다.
Ruby is a lovely language
와 같은 예를 들어보세요. . 문장은 서로 다른 범주의 5개 단어로 구성됩니다. 카테고리를 식별하려면 사전을 가지고 모든 단어의 카테고리를 찾아보면 다음과 같은 목록이 나옵니다. 명사 , 동사 , 기사 , 형용사 , 명사 . 자연어 처리에서는 이러한 부분을 "음성 부분"이라고 합니다. 프로그래밍 언어와 같은 공식 언어에서는 토큰이라고 합니다. .
렉서는 템플릿을 읽고 텍스트 스트림을 주어진 순서의 각 범주에 대한 정규 표현식 세트와 일치시키는 방식으로 작동합니다. 일치하는 첫 번째 항목은 토큰의 범주를 정의하고 관련 데이터를 토큰에 첨부합니다.
이 약간의 이론을 제외하고 템플릿 언어에 대한 렉서를 구현해 보겠습니다. 작업을 조금 더 쉽게 하기 위해 StringScanner
를 사용합니다. strscan
요구 Ruby의 표준 라이브러리에서. (그런데 StringScanner
에 대한 훌륭한 소개가 있습니다. 이전 버전 중 하나에서.) 첫 번째 단계로 모든 것을 CONTENT
로 식별하는 최소 버전을 빌드해 보겠습니다. .
새 StringScanner
를 생성하여 이를 수행합니다. 인스턴스를 만들고 until
를 사용하여 작업을 수행하도록 합니다. 스캐너가 문자열 끝에 도달할 때만 중지되는 루프입니다.
지금은 모든 문자(.*
) 여러 줄( m
수정자) 및 하나의 CONTENT
반환 모든 것에 대한 토큰입니다. 토큰 이름을 첫 번째 요소로, 모든 데이터를 두 번째 요소로 사용하여 토큰을 배열로 나타냅니다. 매우 기본적인 렉서는 다음과 같습니다.
require 'strscan'
module Magicbars
class Lexer
def self.tokenize(code)
new.tokenize(code)
end
def tokenize(code)
scanner = StringScanner.new(code)
tokens = []
until scanner.eos?
tokens << [:CONTENT, scanner.scan(/.*?/m)]
end
tokens
end
end
end
Welcome to {{name}}
와 함께 이 코드를 실행할 때 정확히 하나의 CONTENT
목록을 반환합니다. 모든 코드가 첨부된 토큰입니다.
Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [[:CONTENT, "Welcome to {{name}}"]]
다음으로 표현을 알아봅시다. 이를 위해 루프 내부의 코드를 수정하여 {{
와 일치하도록 합니다. 및 }}
OPEN_EXPRESSION
으로 및 CLOSE
.
다양한 사례를 확인하는 조건을 추가하여 이를 수행합니다.
until scanner.eos?
if scanner.scan(/{{/)
tokens << [:OPEN_EXPRESSION]
elsif scanner.scan(/}}/)
tokens << [:CLOSE]
elsif scanner.scan(/.*?/m)
tokens << [:CONTENT, scanner.matched]
end
end
중괄호를 OPEN_EXPRESSION
에 연결하는 데 추가 가치가 없습니다. 및 CLOSE
토큰을 삭제합니다. scan
으로 이제 호출이 조건의 일부이므로 scanner.matched
를 사용합니다. 마지막 일치 결과를 CONTENT
에 첨부 토큰.
불행히도, 어휘분석기를 다시 실행할 때 여전히 하나의 CONTENT
만 얻습니다. 이전과 같은 토큰. 열린 표현식까지 모든 것과 일치하도록 마지막 표현식을 수정해야 합니다. scan_until
을 사용하여 이 작업을 수행합니다. 바로 앞에 스캐너를 정지시키는 이중 중괄호를 위한 긍정적인 lookahead 앵커가 있습니다. 루프 내부의 코드는 이제 다음과 같습니다.
until scanner.eos?
if scanner.scan(/{{/)
tokens << [:OPEN_EXPRESSION]
elsif scanner.scan(/}}/)
tokens << [:CLOSE]
elsif scanner.scan_until(/.*?(?={{|}})/m)
tokens << [:CONTENT, scanner.matched]
end
end
렉서를 다시 실행하면 이제 4개의 토큰이 생성됩니다.
Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:CONTENT, "name"], [:CLOSE]]
우리의 렉서는 우리가 원하는 결과에 꽤 가깝게 보입니다. 그러나 name
일반 콘텐츠가 아닙니다. 그것은 식별자입니다! 이중 중괄호 사이의 문자열은 외부 문자열과 다르게 취급해야 합니다.
상태 머신
이를 위해 렉서를 두 가지 다른 상태를 가진 상태 머신으로 바꿉니다. default
에서 시작합니다. 상태. OPEN_EXPRESSION
일 때 토큰이 있으면 expression
으로 이동합니다. 상태를 유지하고 CLOSE
가 나타날 때까지 유지 default
로 다시 전환하는 토큰 상태.
배열을 사용하여 현재 상태를 관리하는 몇 가지 메서드를 추가하여 상태 시스템을 구현합니다.
def stack
@stack ||= []
end
def state
stack.last || :default
end
def push_state(state)
stack.push(state)
end
def pop_state
stack.pop
end
state
메소드는 현재 상태 또는 default
를 반환합니다. . push_state
렉서를 스택에 추가하여 렉서를 새 상태로 이동합니다. pop_state
렉서를 이전 상태로 되돌립니다.
다음으로 루프 내에서 조건문을 분할하고 현재 상태를 확인하는 조건문으로 랩핑합니다. default
에 있는 동안 상태, 우리는 OPEN_EXPRESSION
을 모두 처리합니다. 및 CONTENT
토큰. 이것은 또한 CONTENT
에 대한 정규식이 }}
가 필요하지 않습니다. 더 이상 미리보기 때문에 삭제합니다. expression
에서 상태에서 CLOSE
를 처리합니다. 토큰 및 IDENTIFIER
에 대한 새 정규식 추가 . 물론 push_state
를 추가하여 상태 전환도 구현합니다. OPEN_EXPRESSION
호출 및 pop_state
CLOSE
호출 .
if state == :default
if scanner.scan(/{{/)
tokens << [:OPEN_EXPRESSION]
push_state :expression
elsif scanner.scan_until(/.*?(?={{)/m)
tokens << [:CONTENT, scanner.matched]
end
elsif state == :expression
if scanner.scan(/}}/)
tokens << [:CLOSE]
pop_state
elsif scanner.scan(/[\w\-]+/)
tokens << [:IDENTIFIER, scanner.matched]
end
end
이러한 변경 사항이 적용되어 렉서는 이제 예제를 올바르게 토큰화합니다.
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
자신을 더 어렵게 만들기
좀 더 고급 예제로 넘어 갑시다. 이것은 블록뿐만 아니라 여러 표현식을 사용합니다.
Welcome to {{name}}!
{{#if subscribed}}
Thank you for subscribing to our mailing list.
{{else}}
Please sign up for our mailing list to be notified about new articles!
{{/if}}
Your friends at {{company_name}}
렉서가 이 예제를 구문 분석하지 못하는 것은 놀라운 일이 아닙니다. 작동하게 하려면 누락된 토큰을 추가하고 마지막 표현식 뒤에 내용을 처리하도록 해야 합니다. 루프 내부의 코드는 다음과 같습니다.
if state == :default
if scanner.scan(/{{#/)
tokens << [:OPEN_BLOCK]
push_state :expression
elsif scanner.scan(/{{\//)
tokens << [:OPEN_END_BLOCK]
push_state :expression
elsif scanner.scan(/{{else/)
tokens << [:OPEN_INVERSE]
push_state :expression
elsif scanner.scan(/{{/)
tokens << [:OPEN_EXPRESSION]
push_state :expression
elsif scanner.scan_until(/.*?(?={{)/m)
tokens << [:CONTENT, scanner.matched]
else
tokens << [:CONTENT, scanner.rest]
scanner.terminate
end
elsif state == :expression
if scanner.scan(/\s+/)
# Ignore whitespace
elsif scanner.scan(/}}/)
tokens << [:CLOSE]
pop_state
elsif scanner.scan(/[\w\-]+/)
tokens << [:IDENTIFIER, scanner.matched]
else
scanner.terminate
end
end
어느 정도 조건의 순서가 중요하다는 점을 염두에 두시기 바랍니다. 일치하는 첫 번째 정규식이 할당됩니다. 따라서 보다 구체적인 표현이 보다 일반적인 표현보다 먼저 와야 합니다. 이에 대한 대표적인 예는 블록에 대한 특수 공개 토큰 모음입니다.
렉서의 최종 버전을 사용하여 이제 예제는 다음과 같이 토큰화됩니다.
[
[:CONTENT, "Welcome to "],
[:OPEN_EXPRESSION],
[:IDENTIFIER, "name"],
[:CLOSE],
[:CONTENT, "!\n\n"],
[:OPEN_BLOCK],
[:IDENTIFIER, "if"],
[:IDENTIFIER, "subscribed"],
[:CLOSE],
[:CONTENT, "\n Thank you for subscribing to our mailing list.\n"],
[:OPEN_INVERSE],
[:CLOSE],
[:CONTENT, "\n Please sign up for our mailing list to be notified about new articles!\n"],
[:OPEN_END_BLOCK],
[:IDENTIFIER, "if"],
[:CLOSE],
[:CONTENT, "\n\nYour friends at "],
[:OPEN_EXPRESSION],
[:IDENTIFIER, "company_name"],
[:CLOSE],
[:CONTENT, "\n"]
]
이제 완료되었으므로 7가지 유형의 토큰을 식별했습니다.
토큰 | 예 |
---|---|
OPEN_BLOCK | {{# |
OPEN_END_BLOCK | {{/ |
OPEN_INVERSE | {{else |
OPEN_EXPRESSION | {{ |
CONTENT | 표현식 이외의 모든 것(일반 HTML 또는 텍스트) |
CLOSE | }} |
IDENTIFIER | 식별자는 Word 문자, 숫자, _ 로 구성됩니다. , 및 - |
다음 단계는 토큰 스트림의 구조를 파악하고 이를 추상 구문 트리로 변환하는 파서를 구현하는 것입니다. 하지만 이는 다른 시간입니다.
앞으로의 길
우리는 문자열 보간을 사용하여 기본 템플릿 시스템을 구현하는 다양한 방법을 살펴봄으로써 자체 템플릿 언어를 향한 여정을 시작했습니다. 첫 번째 접근 방식의 한계에 도달했을 때 적절한 템플릿 시스템을 구현하기 시작했습니다.
지금은 템플릿을 분석하고 다양한 유형의 토큰을 파악하는 렉서를 구현했습니다. 다음 버전의 Ruby Magic에서는 파서와 인터프리터를 구현하여 보간된 문자열을 생성하는 방식으로 여정을 계속할 것입니다.