Hãy mặc bộ đồ lặn với bình dưỡng khí và đóng gói giấy nến của bạn, hôm nay chúng tôi sẽ đi sâu vào Mẫu!
Hầu hết các phần mềm hiển thị trang web hoặc tạo email đều sử dụng tính năng tạo khuôn mẫu để nhúng dữ liệu biến đổi vào tài liệu văn bản. Cấu trúc chính của tài liệu thường được thiết lập trong một khuôn mẫu tĩnh với các trình giữ chỗ cho dữ liệu. Dữ liệu biến, như tên người dùng hoặc nội dung trang web, thay thế các trình giữ chỗ trong khi hiển thị trang.
Để đi sâu vào tạo mẫu, chúng tôi sẽ triển khai một tập hợp con của Mustache, một ngôn ngữ tạo mẫu có sẵn trong nhiều ngôn ngữ lập trình. Trong tập này, chúng ta sẽ tìm hiểu các cách tạo khuôn mẫu khác nhau. Chúng tôi sẽ bắt đầu xem xét việc nối chuỗi và kết thúc việc viết lexer của riêng chúng tôi để cho phép tạo ra các mẫu phức tạp hơn.
Sử dụng nội suy chuỗi gốc
Hãy bắt đầu với một ví dụ tối thiểu. Ứng dụng của chúng tôi cần một thông báo chào mừng bao gồm tên dự án. Cách nhanh nhất để làm điều này là sử dụng tính năng nội suy chuỗi tích hợp của Ruby.
name = "Ruby Magic"
template = "Welcome to #{name}"
# => Welcome to Ruby Magic
Tuyệt quá! Điều đó có thể làm được. Tuy nhiên, điều gì sẽ xảy ra nếu chúng tôi muốn sử dụng lại mẫu cho nhiều lần hoặc cho phép người dùng cập nhật mẫu?
Nội suy đánh giá ngay lập tức. Chúng tôi không thể sử dụng lại mẫu (trừ khi chúng tôi xác định lại nó — ví dụ:trong một vòng lặp) và chúng tôi không thể lưu trữ Welcome to #{name}
mẫu trong cơ sở dữ liệu và điền nó sau đó mà không sử dụng eval
nguy hiểm tiềm ẩn chức năng.
May mắn thay, Ruby có một cách khác để nội suy chuỗi:Kernel#sprintf
hoặc String#%
. Các phương thức này cho phép chúng ta lấy một chuỗi nội suy mà không cần thay đổi chính mẫu. Bằng cách này, chúng ta có thể sử dụng lại cùng một mẫu nhiều lần. Nó cũng không cho phép thực thi mã Ruby tùy ý. Hãy sử dụng nó.
name = "Ruby Magic"
template = "Welcome to %{name}"
sprintf(template, name: name)
# => "Welcome to Ruby Magic"
template % { name: name }
# => "Welcome to Ruby Magic"
Regexp
Cách tiếp cận để tạo khuôn
Mặc dù giải pháp trên hoạt động, nhưng nó không phải là chống lừa và nó có nhiều chức năng hơn chúng ta thường muốn. Hãy xem một ví dụ:
name = "Ruby Magic"
template = "Welcome to %d"
sprintf(template, name: name)
# => TypeError (can't convert Hash into Integer)
Cả Kernel#sprintf
và String#%
cho phép các cú pháp đặc biệt để xử lý các loại dữ liệu khác nhau. Không phải tất cả chúng đều tương thích với dữ liệu chúng tôi chuyển. Trong ví dụ này, mẫu dự kiến định dạng một số nhưng được chuyển qua một Hash, tạo ra TypeError
.
Nhưng chúng tôi có nhiều công cụ quyền lực hơn trong nhà kho của mình:chúng tôi có thể thực hiện phép nội suy của riêng mình bằng cách sử dụng biểu thức chính quy. Sử dụng biểu thức chính quy cho phép chúng tôi xác định cú pháp tùy chỉnh, như kiểu lấy cảm hứng từ Mustache / Handlebars.
name = "Ruby Magic"
template = "Welcome to {{name}}"
assigns = { "name" => name }
template.gsub(/{{(\w+)}}/) { assigns[$1] }
# => Welcome to Ruby Magic
Chúng tôi sử dụng String#gsub
để thay thế tất cả các trình giữ chỗ (các từ trong dấu ngoặc nhọn kép) bằng giá trị của chúng trong assigns
băm. Nếu không có giá trị tương ứng, phương pháp này sẽ xóa trình giữ chỗ mà không chèn bất kỳ thứ gì.
Thay thế trình giữ chỗ trong một chuỗi như thế này là một giải pháp khả thi cho một chuỗi có một vài trình giữ chỗ. Tuy nhiên, một khi mọi thứ trở nên phức tạp hơn một chút, chúng tôi sẽ nhanh chóng gặp phải vấn đề.
Giả sử chúng ta cần có các điều kiện trong mẫu. Kết quả phải khác nhau dựa trên giá trị của một biến.
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}}
Cụm từ thông dụng không thể xử lý trơn tru trường hợp sử dụng này. Nếu bạn cố gắng đủ, bạn có thể vẫn có thể hack một thứ gì đó cùng nhau, nhưng tại thời điểm này, tốt hơn là bạn nên xây dựng một ngôn ngữ tạo khuôn mẫu phù hợp.
Xây dựng ngôn ngữ tạo mẫu
Việc triển khai một ngôn ngữ tạo khuôn mẫu cũng tương tự như việc triển khai các ngôn ngữ lập trình khác. Cũng giống như một ngôn ngữ kịch bản, một ngôn ngữ khuôn mẫu cần ba thành phần:Một trình ghép nối, một trình phân tích cú pháp và một trình thông dịch. Chúng ta sẽ xem xét từng cái một.
Lexer
Nhiệm vụ đầu tiên chúng ta cần giải quyết được gọi là mã hóa, hay phân tích từ vựng. Quá trình này rất giống với việc xác định các loại từ trong ngôn ngữ tự nhiên.
Lấy một ví dụ như Ruby is a lovely language
. Câu bao gồm năm từ thuộc các loại khác nhau. Để xác định danh mục đó là gì, bạn sẽ lấy từ điển và tra cứu từng danh mục của từ, điều này sẽ dẫn đến danh sách như sau: Danh từ , Động từ , Bài báo , Tính từ , Danh từ . Xử lý ngôn ngữ tự nhiên gọi đây là "Các phần của giọng nói". Trong các ngôn ngữ chính thức - như ngôn ngữ lập trình - chúng được gọi là mã thông báo .
Lexer hoạt động bằng cách đọc mẫu và khớp dòng văn bản với một tập hợp các biểu thức chính quy cho mỗi danh mục theo một thứ tự nhất định. Cái đầu tiên phù hợp xác định danh mục của mã thông báo và đính kèm dữ liệu có liên quan vào nó.
Với chút lý thuyết này, chúng ta hãy triển khai lexer cho ngôn ngữ mẫu của chúng ta. Để làm mọi thứ dễ dàng hơn một chút, chúng tôi sử dụng StringScanner
bằng cách yêu cầu strscan
từ thư viện chuẩn của Ruby. (Nhân tiện, chúng tôi có phần giới thiệu tuyệt vời về StringScanner
trong một trong những phiên bản trước của chúng tôi.) Bước đầu tiên, hãy tạo một phiên bản tối thiểu xác định mọi thứ là CONTENT
.
Chúng tôi thực hiện việc này bằng cách tạo một StringScanner
mới ví dụ và cho phép nó thực hiện công việc của mình bằng cách sử dụng until
vòng lặp chỉ dừng khi máy quét đến cuối chuỗi.
Hiện tại, chúng tôi chỉ để nó khớp với mọi ký tự (.*
) trên nhiều dòng (m
sửa đổi) và trả về một CONTENT
mã thông báo cho tất cả nó. Chúng tôi biểu diễn mã thông báo dưới dạng một mảng với tên mã thông báo là phần tử đầu tiên và bất kỳ dữ liệu nào là phần tử thứ hai. Lexer rất cơ bản của chúng tôi trông giống như thế này:
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
Khi chạy mã này với Welcome to {{name}}
chúng tôi nhận lại danh sách chính xác một CONTENT
mã thông báo với tất cả mã được đính kèm với nó.
Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [[:CONTENT, "Welcome to {{name}}"]]
Tiếp theo, hãy phát hiện biểu thức. Để làm như vậy, chúng tôi sửa đổi mã bên trong vòng lặp để nó khớp với {{
và }}
dưới dạng OPEN_EXPRESSION
và CLOSE
.
Chúng tôi thực hiện việc này bằng cách thêm một điều kiện để kiểm tra các trường hợp khác nhau.
until scanner.eos?
if scanner.scan(/{{/)
tokens << [:OPEN_EXPRESSION]
elsif scanner.scan(/}}/)
tokens << [:CLOSE]
elsif scanner.scan(/.*?/m)
tokens << [:CONTENT, scanner.matched]
end
end
Không có giá trị gia tăng nào khi gắn dấu ngoặc nhọn vào OPEN_EXPRESSION
và CLOSE
mã thông báo, vì vậy chúng tôi đánh rơi chúng. Khi scan
các cuộc gọi hiện là một phần của điều kiện, chúng tôi sử dụng scanner.matched
để đính kèm kết quả của trận đấu cuối cùng vào CONTENT
mã thông báo.
Thật không may, khi chạy lại lexer, chúng tôi vẫn chỉ nhận được một CONTENT
mã thông báo như trước đây. Chúng tôi vẫn phải sửa đổi biểu thức cuối cùng để khớp mọi thứ với biểu thức mở. Chúng tôi thực hiện việc này bằng cách sử dụng scan_until
với một neo hướng nhìn tích cực cho dấu ngoặc nhọn kép dừng máy quét ngay trước chúng. Mã của chúng tôi bên trong vòng lặp bây giờ trông giống như sau:
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
Chạy lại lexer, bây giờ dẫn đến bốn mã thông báo:
Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:CONTENT, "name"], [:CLOSE]]
Lexer của chúng tôi trông khá gần với kết quả chúng tôi muốn. Tuy nhiên, name
không phải là nội dung thông thường; nó là một định danh! Các chuỗi giữa dấu ngoặc nhọn kép phải được xử lý khác với các chuỗi bên ngoài.
Máy trạng thái
Để làm điều này, chúng tôi biến lexer thành một máy trạng thái với hai trạng thái riêng biệt. Nó bắt đầu ở default
tiểu bang. Khi nhấn là OPEN_EXPRESSION
mã thông báo, nó di chuyển đến expression
trạng thái và ở đó cho đến khi gặp CLOSE
mã thông báo làm cho nó chuyển trở lại default
trạng thái.
Chúng tôi triển khai máy trạng thái bằng cách thêm một số phương thức sử dụng mảng để quản lý trạng thái hiện tại.
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
phương thức sẽ trả về trạng thái hiện tại hoặc default
. push_state
chuyển lexer sang trạng thái mới bằng cách thêm nó vào ngăn xếp. pop_state
chuyển lexer trở lại trạng thái trước đó.
Tiếp theo, chúng tôi tách điều kiện trong vòng lặp và bọc nó bằng một điều kiện để kiểm tra trạng thái hiện tại. Trong khi ở default
trạng thái, chúng tôi xử lý cả OPEN_EXPRESSION
và CONTENT
mã thông báo. Điều này cũng có nghĩa là biểu thức chính quy cho CONTENT
không cần }}
nhìn trước mặt nữa, vì vậy chúng tôi bỏ nó. Trong expression
trạng thái, chúng tôi xử lý CLOSE
mã thông báo và thêm một biểu thức chính quy mới cho IDENTIFIER
. Tất nhiên, chúng tôi cũng triển khai các chuyển đổi trạng thái bằng cách thêm push_state
gọi tới OPEN_EXPRESSION
và một pop_state
gọi đến 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
Với những thay đổi này, lexer giờ đây đã mã hóa đúng ví dụ của chúng ta.
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
Tự làm khó mình hơn
Hãy chuyển sang một ví dụ nâng cao hơn. Cái này sử dụng nhiều biểu thức, cũng như một khối.
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}}
Không có gì ngạc nhiên khi lexer của chúng tôi không phân tích cú pháp ví dụ này. Để làm cho nó hoạt động, chúng tôi phải thêm các mã thông báo bị thiếu và làm cho nó xử lý nội dung sau biểu thức cuối cùng. Mã bên trong vòng lặp trông giống như sau:
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
Hãy nhớ rằng thứ tự của các điều kiện là quan trọng ở một mức độ nào đó. Biểu thức chính quy đầu tiên phù hợp được chỉ định. Do đó, các biểu thức cụ thể hơn phải xuất hiện trước những biểu thức chung chung hơn. Ví dụ điển hình về điều này là tập hợp các mã thông báo mở chuyên biệt cho các khối.
Sử dụng phiên bản cuối cùng của lexer, ví dụ hiện được mã hóa thành sau:
[
[: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"]
]
Bây giờ chúng ta đã hoàn thành, chúng ta đã xác định được bảy loại mã thông báo khác nhau:
Token | Ví dụ |
---|---|
OPEN_BLOCK | {{# |
OPEN_END_BLOCK | {{/ |
OPEN_INVERSE | {{else |
OPEN_EXPRESSION | {{ |
CONTENT | Bất kỳ thứ gì bên ngoài biểu thức (HTML hoặc Văn bản thông thường) |
CLOSE | }} |
IDENTIFIER | Số nhận dạng bao gồm các ký tự Word, số, _ và - |
Bước tiếp theo là triển khai một trình phân tích cú pháp để cố gắng tìm ra cấu trúc của luồng mã thông báo và chuyển nó thành một cây cú pháp trừu tượng, nhưng đó là lúc khác.
Con đường phía trước
Chúng tôi bắt đầu hành trình hướng tới ngôn ngữ tạo mẫu của riêng mình bằng cách xem xét các cách khác nhau để triển khai hệ thống tạo mẫu cơ bản sử dụng nội suy chuỗi. Khi chúng tôi đạt đến giới hạn của các phương pháp tiếp cận đầu tiên, chúng tôi bắt đầu triển khai một hệ thống tạo khuôn mẫu phù hợp.
Hiện tại, chúng tôi đã triển khai một lexer phân tích mẫu và tìm ra các loại mã thông báo khác nhau. Trong phiên bản sắp tới của Ruby Magic, chúng ta sẽ tiếp tục hành trình bằng cách triển khai trình phân tích cú pháp cũng như trình thông dịch để tạo chuỗi nội suy.