अपने स्कूबा डाइविंग सूट पर रखो और अपने स्टेंसिल पैक करो, हम आज टेम्पलेट्स में गोता लगा रहे हैं!
अधिकांश सॉफ़्टवेयर जो वेब पेजों को प्रस्तुत करते हैं या ईमेल उत्पन्न करते हैं, वे टेक्स्ट दस्तावेज़ों में चर डेटा को एम्बेड करने के लिए टेम्प्लेटिंग का उपयोग करते हैं। दस्तावेज़ की मुख्य संरचना अक्सर डेटा के लिए प्लेसहोल्डर के साथ एक स्थिर टेम्पलेट में सेट की जाती है। चर डेटा, जैसे उपयोगकर्ता नाम या वेब पेज सामग्री, पेज को रेंडर करते समय प्लेसहोल्डर्स को बदल देते हैं।
टेम्प्लेटिंग में हमारे गोता लगाने के लिए, हम मूंछों का एक सबसेट लागू करेंगे, एक टेम्प्लेटिंग भाषा जो कई प्रोग्रामिंग भाषाओं में उपलब्ध है। इस कड़ी में, हम टेम्प्लेट करने के विभिन्न तरीकों की जांच करेंगे। हम स्ट्रिंग कॉन्सटेनेशन को देखना शुरू करेंगे, और अधिक जटिल टेम्प्लेट की अनुमति देने के लिए अपना स्वयं का लेक्सर लिखना समाप्त करेंगे।
नेटिव स्ट्रिंग इंटरपोलेशन का उपयोग करना
आइए एक न्यूनतम उदाहरण से शुरू करें। हमारे आवेदन को एक स्वागत संदेश की आवश्यकता है जिसमें एक परियोजना का नाम शामिल होता है। ऐसा करने का सबसे तेज़ तरीका रूबी की अंतर्निर्मित स्ट्रिंग इंटरपोलेशन सुविधा का उपयोग करना है।
name = "Ruby Magic"
template = "Welcome to #{name}"
# => Welcome to Ruby Magic
महान! यह साध्य था। हालांकि, क्या होगा यदि हम कई अवसरों के लिए टेम्पलेट का पुन:उपयोग करना चाहते हैं, या अपने उपयोगकर्ताओं को टेम्पलेट को अपडेट करने की अनुमति देना चाहते हैं?
प्रक्षेप तुरंत मूल्यांकन करता है। हम टेम्पलेट का पुन:उपयोग नहीं कर सकते (जब तक कि हम इसे फिर से परिभाषित नहीं करते - उदाहरण के लिए, एक लूप में) और हम Welcome to #{name}
में आपका स्वागत है, संग्रहीत नहीं कर सकते एक डेटाबेस में टेम्पलेट और संभावित खतरनाक eval
. का उपयोग किए बिना इसे बाद में पॉप्युलेट करें समारोह।
सौभाग्य से, रूबी के पास तारों को प्रक्षेपित करने का एक अलग तरीका है:Kernel#sprintf
या String#%
. ये विधियां हमें टेम्पलेट को बदले बिना एक इंटरपोलेटेड स्ट्रिंग प्राप्त करने की अनुमति देती हैं। इस तरह, हम एक ही टेम्पलेट को कई बार पुन:उपयोग कर सकते हैं। यह मनमानी रूबी कोड के निष्पादन की भी अनुमति नहीं देता है। आइए इसका इस्तेमाल करें।
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
उत्पन्न करते हुए एक हैश पास हो जाता है ।
लेकिन हमारे शेड में अधिक बिजली उपकरण हैं:हम नियमित अभिव्यक्तियों का उपयोग करके अपने स्वयं के प्रक्षेप को लागू कर सकते हैं। रेगुलर एक्सप्रेशन का उपयोग करने से हम कस्टम सिंटैक्स को परिभाषित कर सकते हैं, जैसे मूंछें/हैंडलबार से प्रेरित शैली।
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
. वाक्य में विभिन्न श्रेणियों के पाँच शब्द हैं। यह पहचानने के लिए कि वे किस श्रेणी के हैं, आप एक शब्दकोष लेंगे और प्रत्येक शब्द की श्रेणी देखेंगे, जिसके परिणामस्वरूप एक सूची इस प्रकार होगी:संज्ञा , क्रिया , अनुच्छेद , विशेषण , संज्ञा . प्राकृतिक भाषा प्रसंस्करण इन्हें "भाषण के भाग" कहते हैं। औपचारिक भाषाओं में--जैसे प्रोग्रामिंग भाषाएं-- उन्हें टोकन . कहा जाता है ।
एक लेक्सर किसी दिए गए क्रम में प्रत्येक श्रेणी के लिए नियमित अभिव्यक्तियों के एक सेट के साथ टेम्पलेट को पढ़कर और पाठ की धारा का मिलान करके काम करता है। पहला जो मेल खाता है वह टोकन की श्रेणी को परिभाषित करता है और उसमें प्रासंगिक डेटा संलग्न करता है।
इस छोटे से सिद्धांत के साथ, आइए अपनी टेम्पलेट भाषा के लिए एक लेक्सर लागू करें। चीजों को थोड़ा आसान बनाने के लिए, हम StringScanner
. का उपयोग करते हैं strscan
. की आवश्यकता के द्वारा रूबी के मानक पुस्तकालय से। (वैसे, हमारे पास 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
. के लिए एक नया रेगुलर एक्सप्रेशन जोड़ें . बेशक, हम एक 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"]
]
अब जब हम समाप्त कर चुके हैं, तो हमने सात अलग-अलग प्रकार के टोकन की पहचान कर ली है:
टोकन | उदाहरण |
---|---|
OPEN_BLOCK | {{# |
OPEN_END_BLOCK | {{/ |
OPEN_INVERSE | {{else |
OPEN_EXPRESSION | {{ |
CONTENT | अभिव्यक्तियों के बाहर कुछ भी (सामान्य HTML या टेक्स्ट) |
CLOSE | }} |
IDENTIFIER | पहचानकर्ता में शब्द वर्ण, संख्याएं, _ होते हैं , और - |
अगला कदम एक पार्सर को लागू करना है जो टोकन स्ट्रीम की संरचना का पता लगाने की कोशिश करता है और इसे एक सार सिंटैक्स ट्री में अनुवाद करता है, लेकिन यह एक और समय के लिए है।
आगे का रास्ता
हमने स्ट्रिंग इंटरपोलेशन का उपयोग करके एक बुनियादी टेम्प्लेटिंग सिस्टम को लागू करने के विभिन्न तरीकों को देखकर अपनी खुद की टेम्प्लेटिंग भाषा की ओर अपनी यात्रा शुरू की। जब हमने पहले तरीकों की सीमा पार कर ली, तो हमने एक उचित टेंपलेटिंग सिस्टम को लागू करना शुरू कर दिया।
अभी के लिए, हमने एक लेक्सर लागू किया है जो टेम्पलेट का विश्लेषण करता है और विभिन्न प्रकार के टोकन का पता लगाता है। रूबी मैजिक के आगामी संस्करण में, हम एक इंटरपोलेटेड स्ट्रिंग उत्पन्न करने के लिए एक पार्सर के साथ-साथ एक दुभाषिया को लागू करके यात्रा जारी रखेंगे।