สวมชุดดำน้ำและแพ็คลายฉลุของคุณ เรากำลังดำดิ่งสู่เทมเพลตวันนี้!
ซอฟต์แวร์ส่วนใหญ่ที่แสดงหน้าเว็บหรือสร้างอีเมลใช้เทมเพลตเพื่อฝังข้อมูลตัวแปรลงในเอกสารข้อความ โครงสร้างหลักของเอกสารมักถูกตั้งค่าในเทมเพลตแบบคงที่พร้อมตัวยึดตำแหน่งสำหรับข้อมูล ข้อมูลตัวแปร เช่น ชื่อผู้ใช้หรือเนื้อหาของหน้าเว็บ จะแทนที่ตัวยึดตำแหน่งในขณะที่แสดงหน้าเว็บ
สำหรับการดำดิ่งสู่การสร้างเทมเพลต เราจะใช้ชุดย่อยของ Mustache ซึ่งเป็นภาษาเทมเพลตที่มีอยู่ในภาษาโปรแกรมหลายภาษา ในตอนนี้ เราจะตรวจสอบวิธีการสร้างเทมเพลตต่างๆ เราจะเริ่มมองหาการต่อสตริง และลงเอยด้วยการเขียน lexer ของเราเองเพื่อให้มีเทมเพลตที่ซับซ้อนมากขึ้น
การใช้ Native String Interpolation
เริ่มต้นด้วยตัวอย่างน้อยที่สุด ใบสมัครของเราต้องการข้อความต้อนรับที่มีชื่อโครงการ วิธีที่รวดเร็วที่สุดในการทำเช่นนี้คือการใช้คุณสมบัติการแก้ไขสตริงในตัวของ 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"
The Regexp
แนวทางการทำเทมเพลท
แม้ว่าวิธีแก้ปัญหาข้างต้นจะได้ผล แต่ก็ไม่สามารถพิสูจน์ได้ และเปิดเผยฟังก์ชันการทำงานมากกว่าที่เราต้องการ มาดูตัวอย่างกัน:
name = "Ruby Magic"
template = "Welcome to %d"
sprintf(template, name: name)
# => TypeError (can't convert Hash into Integer)
ทั้ง Kernel#sprintf
และ String#%
อนุญาตให้ใช้รูปแบบพิเศษในการจัดการข้อมูลประเภทต่างๆ ทั้งหมดไม่เข้ากันกับข้อมูลที่เราส่ง ในตัวอย่างนี้ เทมเพลตต้องการจัดรูปแบบตัวเลขแต่ส่งผ่าน Hash ทำให้เกิด TypeError
.
แต่เรามีเครื่องมือไฟฟ้ามากกว่าในโรงเก็บของ:เราสามารถใช้การแก้ไขของเราเองโดยใช้นิพจน์ทั่วไป การใช้นิพจน์ทั่วไปช่วยให้เรากำหนดไวยากรณ์ที่กำหนดเองได้ เช่น สไตล์ที่ได้รับแรงบันดาลใจจากหนวด/แฮนด์บาร์
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}}
นิพจน์ทั่วไปไม่สามารถจัดการกับกรณีการใช้งานนี้ได้อย่างราบรื่น หากคุณพยายามมากพอ คุณอาจจะยังแฮ็กข้อมูลบางอย่างร่วมกันได้ แต่ ณ จุดนี้ จะดีกว่าที่จะสร้างภาษาเทมเพลตที่เหมาะสม
การสร้างภาษาเทมเพลต
การใช้ภาษาเทมเพลตนั้นคล้ายกับการนำภาษาโปรแกรมอื่นๆ ไปใช้ เช่นเดียวกับภาษาสคริปต์ ภาษาเทมเพลตต้องมีองค์ประกอบสามอย่าง:lexer, parser และ interpreter เราจะดูสิ่งเหล่านี้ทีละตัว
เล็กเซอร์
งานแรกที่เราต้องจัดการเรียกว่า tokenization หรือ lexical analysis กระบวนการนี้คล้ายกับการระบุหมวดหมู่คำในภาษาธรรมชาติมาก
ยกตัวอย่างเช่น Ruby is a lovely language
. ประโยคประกอบด้วยห้าคำในประเภทที่แตกต่างกัน ในการระบุว่าเป็นหมวดหมู่ใด คุณจะต้องใช้พจนานุกรมและค้นหาทุกหมวดหมู่ของคำ ซึ่งจะส่งผลให้รายการมีลักษณะดังนี้:คำนาม , กริยา , บทความ , คำคุณศัพท์ , คำนาม . การประมวลผลภาษาธรรมชาติเรียกว่า "ส่วนของคำพูด" ในภาษาที่เป็นทางการ เช่น ภาษาโปรแกรม เรียกว่า โทเค็น .
lexer ทำงานโดยการอ่านเทมเพลตและจับคู่กระแสข้อความกับชุดนิพจน์ทั่วไปสำหรับแต่ละหมวดหมู่ตามลำดับที่กำหนด รายการแรกที่ตรงกันจะกำหนดหมวดหมู่ของโทเค็นและแนบข้อมูลที่เกี่ยวข้องเข้าไป
ด้วยทฤษฎีเล็กๆ น้อยๆ นี้ เรามาปรับใช้ lexer สำหรับภาษาเทมเพลตของเรา เพื่อให้ง่ายขึ้นอีกเล็กน้อย เราใช้ StringScanner
โดยกำหนดให้ strscan
จากไลบรารีมาตรฐานของ Ruby (อีกอย่าง เรามีบทนำที่ยอดเยี่ยมสำหรับ StringScanner
ในฉบับก่อนหน้าของเรา) ขั้นแรก เรามาสร้างเวอร์ชันขั้นต่ำที่ระบุว่าทุกอย่างเป็น CONTENT
.
เราทำได้โดยการสร้าง StringScanner
. ใหม่ อินสแตนซ์และปล่อยให้มันทำงานโดยใช้ until
ลูปที่หยุดเมื่อสแกนเนอร์ถึงจุดสิ้นสุดของสตริงเท่านั้น
ตอนนี้เราปล่อยให้มันตรงกับทุกตัวอักษร (.*
) ข้ามหลายบรรทัด (m
ตัวแก้ไข) และส่งคืน CONTENT
. หนึ่งรายการ โทเค็นสำหรับมันทั้งหมด เราแสดงโทเค็นเป็นอาร์เรย์ที่มีชื่อโทเค็นเป็นองค์ประกอบแรกและข้อมูลใดๆ เป็นองค์ประกอบที่สอง lexer พื้นฐานของเรามีลักษณะดังนี้:
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
โทเค็น
ขออภัย เมื่อเรียกใช้ lexer อีกครั้ง เรายังคงได้รับ CONTENT
เพียงรายการเดียว โทเค็นเหมือนก่อน เรายังต้องแก้ไขนิพจน์สุดท้ายเพื่อให้ตรงกับทุกนิพจน์ที่เปิดอยู่ เราทำสิ่งนี้โดยใช้ scan_until
ด้วยจุดยึดหัวมองที่เป็นบวกสำหรับเหล็กจัดฟันแบบลอนคู่ที่หยุดเครื่องสแกนตรงหน้าพวกเขา ตอนนี้โค้ดของเราในลูปมีลักษณะดังนี้:
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
เรียกใช้ lexer อีกครั้ง ส่งผลให้มีสี่โทเค็น:
Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:CONTENT, "name"], [:CLOSE]]
lexer ของเราดูค่อนข้างใกล้เคียงกับผลลัพธ์ที่เราต้องการ อย่างไรก็ตาม name
ไม่ใช่เนื้อหาปกติ มันเป็นตัวระบุ! สตริงระหว่างวงเล็บปีกกาคู่ควรได้รับการปฏิบัติที่แตกต่างจากสตริงภายนอก
เครื่องรัฐ
ในการทำเช่นนี้ เราเปลี่ยน lexer ให้เป็นเครื่องของรัฐที่มีสถานะต่างกันสองสถานะ มันเริ่มต้นใน default
สถานะ. เมื่อถึง OPEN_EXPRESSION
token จะย้ายไปที่ 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
method จะคืนค่าสถานะปัจจุบันหรือ default
. push_state
ย้าย lexer ไปสู่สถานะใหม่โดยเพิ่มลงในสแต็ก pop_state
ย้าย lexer กลับสู่สถานะก่อนหน้า
ต่อไป เราแยกเงื่อนไขภายในลูปและห่อด้วยเงื่อนไขที่ตรวจสอบสถานะปัจจุบัน ขณะที่อยู่ใน 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
เมื่อมีการเปลี่ยนแปลงเหล่านี้ lexer ก็สร้างโทเค็นให้กับตัวอย่างของเราได้อย่างเหมาะสม
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}}
ไม่น่าแปลกใจเลยที่ lexer ของเราไม่สามารถแยกวิเคราะห์ตัวอย่างนี้ได้ เพื่อให้ใช้งานได้ เราต้องเพิ่มโทเค็นที่หายไป และทำให้จัดการเนื้อหาหลังนิพจน์สุดท้าย รหัสภายในลูปมีลักษณะดังนี้:
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
โปรดทราบว่าลำดับของเงื่อนไขมีความสำคัญในระดับหนึ่ง มีการกำหนดนิพจน์ทั่วไปแรกที่ตรงกัน ดังนั้น สำนวนที่เจาะจงกว่าจึงต้องมาก่อนนิพจน์ทั่วไป ตัวอย่างที่สำคัญของสิ่งนี้คือคอลเล็กชันโทเค็นเปิดเฉพาะสำหรับบล็อก
เมื่อใช้ lexer เวอร์ชันสุดท้าย ตัวอย่างจะแปลงเป็นสิ่งนี้:
[
[: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"]
]
เมื่อเสร็จแล้ว เราได้ระบุโทเค็นที่แตกต่างกันเจ็ดประเภท:
โทเค็น | ตัวอย่าง |
---|---|
OPEN_BLOCK | {{# |
OPEN_END_BLOCK | {{/ |
OPEN_INVERSE | {{else |
OPEN_EXPRESSION | {{ |
CONTENT | สิ่งที่อยู่นอกนิพจน์ (HTML ปกติหรือข้อความ) |
CLOSE | }} |
IDENTIFIER | ตัวระบุประกอบด้วยอักขระ Word ตัวเลข _ และ - |
ขั้นตอนต่อไปคือการใช้ parser ที่พยายามหาโครงสร้างของสตรีมโทเค็นและแปลเป็นแผนผังไวยากรณ์ที่เป็นนามธรรม แต่นั่นเป็นอีกครั้ง
ถนนข้างหน้า
เราเริ่มต้นการเดินทางสู่ภาษาเทมเพลตของเราโดยดูวิธีต่างๆ ในการใช้ระบบเทมเพลตพื้นฐานโดยใช้การแก้ไขสตริง เมื่อถึงขีดจำกัดของแนวทางแรก เราก็เริ่มใช้ระบบเทมเพลตที่เหมาะสม
สำหรับตอนนี้ เราได้ใช้ lexer ที่วิเคราะห์เทมเพลตและค้นหาโทเค็นประเภทต่างๆ ใน Ruby Magic ฉบับต่อไป เราจะเดินทางต่อโดยใช้โปรแกรมแยกวิเคราะห์และล่ามเพื่อสร้างสตริงที่สอดแทรก