अपना स्कूबा डाइविंग सूट पहनें और अपने स्टेंसिल पैक करें, हम आज टेम्पलेट्स में गोता लगा रहे हैं!
वेब पेज रेंडर करने या ईमेल जनरेट करने वाले अधिकांश सॉफ़्टवेयर टेक्स्ट डॉक्यूमेंट में वेरिएबल डेटा एम्बेड करने के लिए टेम्पलेटिंग का उपयोग करते हैं। डॉक्यूमेंट की मुख्य संरचना अक्सर प्लेसहोल्डर्स के साथ एक स्टेटिक टेम्पलेट में सेट की जाती है। वेरिएबल डेटा, जैसे उपयोगकर्ता नाम या वेब पेज कंटेंट, पेज रेंडर करते समय प्लेसहोल्डर्स को प्रतिस्थापित करते हैं।
टेम्पलेटिंग में हमारे गोता के लिए, हम 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#% दोनों अलग-अलग प्रकार के डेटा को संभालने के लिए विशेष सिंटैक्स की अनुमति देते हैं। उनमें से सभी हमारे द्वारा पास किए गए डेटा के साथ संगत नहीं हैं। इस उदाहरण में, टेम्पलेट एक नंबर को फॉर्मेट करने की अपेक्षा करता है लेकिन एक Hash पास किया जाता है, जिससे TypeError उत्पन्न होता है।
लेकिन हमारे शेड में अधिक पावर टूल्स हैं: हम रेगुलर एक्सप्रेशन का उपयोग करके अपना खुद का इंटरपोलेशन लागू कर सकते हैं। रेगुलर एक्सप्रेशन का उपयोग करने से हम एक कस्टम सिंटैक्स को परिभाषित कर सकते हैं, जैसे Mustache/Handlebars प्रेरित शैली।
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 जैसे उदाहरण को लें। वाक्य में विभिन्न श्रेणियों के पांच शब्द होते हैं। उनकी श्रेणी की पहचान करने के लिए, आप एक डिक्शनरी लेंगे और प्रत्येक शब्द की श्रेणी को देखेंगे, जिसके परिणामस्वरूप ऐसी सूची मिलेगी: Noun, Verb, Article, Adjective, Noun। प्राकृतिक भाषा प्रसंस्क इन्हें "Parts of Speech" कहता है। औपचारिक भाषाओं में—जैसे प्रोग्रामिंग भाषाएं—उन्हें टोकन कहा जाता है।
एक लेक्सर टेम्पलेट को पढ़कर और टेक्स्ट की स्ट्रीम को प्रत्येक श्रेणी के लिए रेगुलर एक्सप्रेशन के एक सेट के साथ दिए गए क्रम में मिलान करके काम करता है। पहला जो मेल खाता है वह टोकन की श्रेणी को परिभाषित करता है और उससे प्रासंगिक डेटा जोड़ता है।
थोड़े से सिद्धांत के साथ, आइए अपने टेम्पलेट भाषा के लिए एक लेक्सर लागू करें। चीजों को थोड़ा आसान बनाने के लिए, हम Ruby के मानक पुस्तकालय से strscan को require करके StringScanner का उपयोग करते हैं। (वैसे, हमारे पिछले संस्करणों में से एक में 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 का उपयोग डबल कर्ली ब्रेसेस के लिए पॉजिटिव लुकअहेड एंकर के साथ करते हैं जो स्कैनर को उनके ठीक पहले रोक देता है। लूप के अंदर हमारा कोड अब इस तरह दिखता है:
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
लेक्सर को फिर से चलाने पर, अब चार टोकन परिणामित होते हैं:
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 के लिए एक नया रेगुलर एक्सप्रेशन जोड़ते हैं। बेशक, हम 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
इन परिवर्तनों के साथ, लेक्सर अब हमारे उदाहरण को सही ढंग से टोकनाइज़ करता है।
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"]
]
अब जब हम समाप्त कर चुके हैं, तो हमने सात अलग-अलग प्रकार के टोकन की पहचान की है:
| टोकन | उदाहरण |
|---|---|
OPEN_BLOCK |
{{# |
OPEN_END_BLOCK |
{{/ |
OPEN_INVERSE |
{{else |
OPEN_EXPRESSION |
{{ |
CONTENT |
एक्सप्रेशन के बाहर कुछ भी (सामान्य HTML या टेक्स्ट) |
CLOSE |
}} |
IDENTIFIER |
पहचानकर्ता में वर्ड कैरेक्टर, नंबर, _, और - होते हैं |
अगला कदम एक पार्सर को लागू करना है जो टोकन स्ट्रीम की संरचना का पता लगाने की कोशिश करता है और इसे एक abstract syntax tree में अनुवाद करता है, लेकिन यह दूसरी बार के लिए है।
आगे का रास्ता
हमने स्ट्रिंग इंटरपोलेशन का उपयोग करके एक बुनियादी टेम्पलेटिंग सिस्टम को लागू करने के विभिन्न तरीकों को देखकर अपनी टेम्पलेटिंग भाषा की दिशा में अपनी यात्रा शुरू की। जब हम पहले दृष्टिकोण की सीमाओं से टकराए, तो हमने एक उचित टेम्पलेटिंग सिस्टम को लागू करना शुरू किया।
अभी के लिए, हमने एक लेक्सर लागू किया जो टेम्पलेट का विश्लेषण करता है और विभिन्न प्रकार के टोकन का पता लगाता है। Ruby Magic के आगामी संस्करण में, हम एक पार्सर और साथ ही एक इंटरप्रेटर को लागू करके यात्रा जारी रखेंगे ताकि एक इंटरपोलेटेड स्ट्रिंग उत्पन्न हो सके।