Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การสร้าง Lexer เทมเพลต Ruby ที่แข็งแกร่งสำหรับการเรนเดอร์เว็บแบบไดนามิก

สวมชุดดำน้ำและห่ออุปกรณ์สแตนซิล เรากำลังดำน้ำเข้าไปใน Templates วันนี้!

ซอฟต์แวร์ส่วนใหญ่ที่ render หน้าเว็บหรือสร้างอีเมลใช้ templating เพื่อฝังข้อมูลตัวแปรลงในเอกสารข้อความ โครงสร้างหลักของเอกสารถูกตั้งค่าใน static template พร้อม placeholders สำหรับข้อมูล ข้อมูลตัวแปร เช่น ชื่อผู้ใช้หรือเนื้อหาหน้าเว็บ แทนที่ placeholders ขณะ render หน้า

สำหรับการดำน้ำของเราใน templating เราจะ implement subset ของ Mustache ภาษาเทมเพลตที่มีในหลายภาษาโปรแกรมมิ่ง ในตอนนี้ เราจะสำรวจวิธีการ templating ต่างๆ เราจะเริ่มจาก string concatenation และจบลงที่การเขียน lexer ของตัวเองเพื่ออนุญาตให้เทมเพลตซับซ้อนขึ้น

การใช้ Native String Interpolation

เริ่มด้วยตัวอย่าง minimal แอปของเราต้องการ welcome message ที่碰巧 包含 project name วิธีเร็วที่สุดคือใช้ built-in string interpolation ของ Ruby

name = "Ruby Magic" template = "Welcome to #{name}" # => Welcome to Ruby Magic

ยอดเยี่ยม! ทำได้ แต่มاذاถ้าเราอยาก reuse เทมเพลตสำหอนโอกาสหลายครั้ง หรืออนุญาตให้ผู้ใช้ update เทมเพลต?

Interpolation ประเมินค่าทันที เราไม่สามารถ reuse เทมเพลตได้ (เว้นแต่เราจะ redefine มัน—ใน loop ตัวอย่าง) และเราไม่สามารถเก็บเทมเพลต Welcome to #{name} ในฐานข้อมูลและ populate มันภายหลังโดยไม่ใช้ eval function ที่อันตราย

โชคดีที่ Ruby มีวิธีอีกอย่างในการ interpolate สตริง: Kernel#sprintf หรือ String#% เมธอดเหล่านี้ช่วยให้เราได้ interpolated string โดยไม่เปลี่ยนเทมเพลตเอง วิธีนี้เราสามารถ reuse เทมเพลตเดียวกันหลายครั้งได้ มันก็ไม่ยอมให้รัน Ruby code แบบ arbitrary มาลองใช้

name = "Ruby Magic" template = "Welcome to %{name}" sprintf(template, name: name) # => "Welcome to Ruby Magic" template % { name: name } # => "Welcome to Ruby Magic"

แนวทาง Regexp สำหรับ Templating

ในขณะที่วิธีข้างต้นทำงาน มันไม่ได้ fool-proof และ expose functionality มากกว่าที่เราต้องการปกติ มาดูตัวอย่าง:

name = "Ruby Magic" template = "Welcome to %d" sprintf(template, name: name) # => TypeError (can't convert Hash into Integer)

ทั้ง Kernel#sprintf และ String#% อนุญาต syntax พิเศษเพื่อจัดการประเภทข้อมูลต่างๆ ไม่ใช่ทุกอย่าง compatible กับข้อมูลที่เราส่ง ในตัวอย่างนี้ เทมเพลตคาดหวัง format ตัวเลขแต่ได้รับ Hash ทำให้เกิด TypeError

แตเรามี power tools มากกว่าในโรงเก็บ: เราสามารถ implement interpolation ของตัวเองโดยใช้ regular expressions การใช้ regular expressions ช่วยให้เรากำหนด custom syntax ได้ เช่น Mustache/Handlebars inspired style

name = "Ruby Magic" template = "Welcome to {{name}}" assigns = { "name" => name } template.gsub(/{{(\w+)}}/) { assigns[$1] } # => Welcome to Ruby Magic

เราใช้ String#gsub เพื่อแทนที่ placeholders ทั้งหมด (คำใน double curly braces) ด้วยค่าของมันใน hash assigns หากไม่มีค่าที่ตรงกัน เมธอดนี้จะลบ placeholder โดยไม่แทรกอะไร

การแทนที่ placeholders ในสตริงแบบนี้เป็น viable solution สำหรับสตริงที่มี placeholders สองสามตัว แต่เมื่อสิ่งต่างๆ ซับซ้อนขึ้น เราเร็วๆ นี้จะเจอปัญหา

สมมติว่าเราต้องการ conditionals ในเทมเพลต ผลลัพธ์ควรแตกต่างกันขึ้นอยู่กับค่าของตัวแปร

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}}

Regular expressions ไม่สามารถจัดการ use case นี้ได้อย่างราบรื่น หากคุณพยายามพอดี คุณอาจ hack something together ได้ แต่ในจุดนี้ 好 是 建立 一個 適當 的 模板 語言

การสร้างภาษาเทมเพลต

การ implement ภาษาเทมเพลตคล้ายกับการ implement ภาษาโปรแกรมมิ่งอื่นๆ เหมือนกับ scripting language ภาษาเทมเพลตต้องการสาม components: Lexer, Parser, และ Interpreter เราจะดูทีละอย่าง

Lexer

งานแรกที่เราต้องจัดการเรียกว่า tokenization หรือ lexical analysis กระบวนการนี้คล้ายกับการระบุหมวดหมู่คำในภาษาธรรมชาติ

นำตัวอย่าง Ruby is a lovely language ประโยคประกอบด้วยคำห้าคำหมวดหมู่ต่างกัน เพื่อระบุว่าพวกมันเป็นหมวดหมู่อะไร คุณจะนำ dictionary ไป lookup หมวดหมู่ของแต่ละคำ ซึ่งจะได้ list แบบนี้: Noun, Verb, Article, Adjective, Noun Natural language processing เรียกว่า "Parts of Speech" ใน formal languages—เช่น programming languages—พวกมันเรียกว่า tokens

Lexer ทำงานโดยอ่านเทมเพลตและ match stream ของข้อความกับชุด regular expressions สำหรับแต่ละหมวดหมู่ในลำดับที่กำหนด ตัวแรกที่ match กำหนดหมวดหมู่ของ token และ attach ข้อมูลที่เกี่ยวข้อง

ด้วยทฤษฎีเล็กน้อยนี้พอ มา implement lexer สำหรับภาษาเทมเพลตของเรา เพื่อให้ง่ายขึ้น เราใช้ StringScanner โดย require strscan จาก standard library ของ Ruby (เรื่องราว เรามี intro เยี่ยมเกี่ยวกับ StringScanner ใน editions ก่อนหน้า) ขั้นแรก มาสร้าง minimal version ที่ระบุทุกอย่างเป็น CONTENT

เราทำโดยสร้าง instance StringScanner ใหม่และปล่อยให้มันทำงานด้วย until loop ที่หยุดก็ต่อเมื่อ scanner ถึง end ของ string

ตอนนี้ เราแค่ปล่อยให้มัน match ทุก character (.*) ข้ามหลายบรรทัด (modifier m) และคืนค่า token CONTENT เดียวสำหรับทั้งหมด เรา represent token เป็น array ที่มี token name เป็น element แรก และข้อมูลใดๆ เป็น element ที่สอง 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}} เราได้กลับ list ของ CONTENT token เดียวพร้อมโค้ดทั้งหมด attached

Magicbars::Lexer.tokenize("Welcome to {{name}}") => [[:CONTENT, "Welcome to {{name}}"]]

ต่อไป มาตรวจจับ expression เพื่อทำเช่นนั้น เราแก้ไขโค้ดภายใน loop ให้ match {{ และ }} เป็น OPEN_EXPRESSION และ CLOSE

เราทำโดยเพิ่ม conditional ที่ตรวจสอบกรณีต่างๆ

until scanner.eos? if scanner.scan(/{{/) tokens << [:OPEN_EXPRESSION] elsif scanner.scan(/}}/) tokens << [:CLOSE] elsif scanner.scan(/.*?/m) tokens << [:CONTENT, scanner.matched] end end

ไม่มีค่าเพิ่มในการ attach curly braces ให้ tokens OPEN_EXPRESSION และ CLOSE 所以 我们 丢弃 它们 由于 scan calls 现在 是 条件 的 一部分 我们 使用 scanner.matched 来 attach 结果 of the last match 到 CONTENT token

不幸的是 当 重新 运行 lexer 我们 仍然 只 得到 一个 CONTENT token 像 以前 一样 我们 仍然 必须 修改 最后 的 表达式 以 匹配 一切 直到 open expression 我们 通过 使用 scan_until 配合 positive lookahead anchor สำหรับ double curly braces ที่หยุด scanner ไว้ก่อนพวกมัน โค้ดภายใน loop ตอนนี้เป็นแบบนี้:

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 อีกครั้ง ตอนนี้ได้ 4 tokens:

Magicbars::Lexer.tokenize("Welcome to {{name}}") => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:CONTENT, "name"], [:CLOSE]]

Lexer ของเราดูใกล้เคียงผลลัพธ์ที่เราต้องการ แต่ name ไม่ใช่ regular content มันคือ identifier! สตริงระหว่าง double curly braces ควรถูกจัดการต่างจากสตริงภายนอก

State Machine

เพื่อทำเช่นนั้น เราเปลี่ยน lexer เป็น state machine ที่มีสองสถานะที่แตกต่าง มันเริ่มในสถานะ default เมื่อมันเจอ token OPEN_EXPRESSION มันเคลื่อนไปสถานะ expression และอยู่ที่นั่นจนกว่าจะเจอ token CLOSE ซึ่งทำให้มัน transition กลับไป default

เรา implement state machine โดยเพิ่มเมธอดสองสามที่ใช้ array จัดการ current state

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 จะคืนค่า current state หรือ default push_state ย้าย lexer เข้า state ใหม่โดยเพิ่มเข้า stack pop_state ย้าย lexer กลับ previous state

ต่อไป เราแยก conditional ภายใน loop และ wrap ด้วย conditional ที่ตรวจสอบ current state ในขณะที่ในสถานะ default เราจัดการทั้ง OPEN_EXPRESSION และ CONTENT tokens นี้หมายความว่า regular expression สำหรับ CONTENT ไม่ต้องการ }} lookahead อีกต่อไป เรา drop มัน ในสถานะ expression เราจัดการ token CLOSE และเพิ่ม regular expression ใหม่สำหรับ IDENTIFIER 當然 我们 也 实现 状态 转换 通过 向 OPEN_EXPRESSION 添加 push_state 调用 和 向 CLOSE 添加 pop_state 调用

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 ตอนนี้ tokenize ตัวอย่างของเราได้ถูกต้อง

Magicbars::Lexer.tokenize("Welcome to {{name}}") # => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]

ทำให้ยากขึ้นสำหรับตัวเอง

มาไปที่ตัวอย่าง advanced มากขึ้น ตัวนี้ใช้ multiple expressions รวมถึง block

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 ของเราล้มเหลวในการ parse ตัวอย่างนี้ เพื่อให้ทำงาน เราต้องเพิ่ม tokens ที่ขาดหายไปและทำให้มันจัดการ content หลัง expression สุดท้าย โค้ดภายใน loop เป็นแบบนี้:

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

โปรดจำว่าลำดับของ conditions สำคัญในระดับหนึ่ง Regular expression แรกที่ match จะถูก assign ดังนั้น expressions ที่เฉพาะเจาะจงกว่าต้องมาก่อน generic ones ตัวอย่างหลักคือ collection ของ specialized open tokens สำหรับ blocks

ใช้ final version ของ lexer ตัวอย่างตอนนี้ tokenize เป็น:

[ [: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"] ]

ตอนที่เราเสร็จ เราระบุ token types 7 แบบ:

TokenExample
OPEN_BLOCK{{#
OPEN_END_BLOCK{{/
OPEN_INVERSE{{else
OPEN_EXPRESSION{{
CONTENTAnything outside of expressions (normal HTML or Text)
CLOSE}}
IDENTIFIERIdentifiers consist of Word characters, numbers, _, and -

ขั้นตอนต่อไปคือ implement parser ที่พยายาม find out โครงสร้างของ token stream และแปลงเป็น abstract syntax tree แต่เรื่องนั้นจะเป็นในอีกครั้ง

ถนนข้างหน้า

เราเริ่มการเดินทางสู่ภาษาเทมเพลตของตัวเองโดยดูวิธีต่างๆ ในการ implement basic templating system ใช้ string interpolation เมื่อเราเจอขีดจำกัดของแนวทางแรก เราเริ่ม implement templating system ที่เหมาะสม

ตอนนี้ เรา implement lexer ที่วิเคราะห์เทมเพลตและหาประเภท tokens ต่างๆ ใน Ruby Magic 版本 即将 推出 我们 将 继续 旅程 通过 实现 parser 以及 interpreter 来 生成 插值 字符串