কম্পিউটার টিউটোরিয়াল

ডাইনামিক ওয়েব রেন্ডারিং-এর জন্য একটি শক্তিশালী রুবি টেমপ্লেট লেক্সার তৈরি করা

আপনার স্কুবা ডাইভিং সুইট पहনুন এবং আপনার স্টেনসিল প্যাক করুন, আমরা আজ টেমপ্লেটগুলোর মধ্যে ডুব করছি!

বেশিরাগ সফ্টওয়ার যা ওয়েব পেজ রেন্ডার করে বা ইমেইল জেনারেট করে, টেমপ্লেটিং ব্যবহার করে ভেরিয়েবল ডেটা টেক্সট ডকুমেন্টে এম্বেড করতে। ডকুমেন্টের মূল কাঠামো প্রায়শই একটি স্ট্যাটিক টেমপ্লেটে সেট আপ করা থাকে প্লেসহোল্ডারসহ ডেটার জন্য। ভেরিয়েবল ডেটা, যেন ইউজার নাম বা ওয়েব পেজ কন্টেন্ট, পেজ রেন্ডার করার সময় প্লেসহোল্ডারগুলো রিপ্লেস করে। আমাদের টেমপ্লেটিং-এ ডুবের জন্য, আমরা Mustache-এর একটি সাবসেট ইমপ্লিমেন্ট করব, একটি টেমপ্লেটিং ল্যাঙ্গুয়েজ যা অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজে উপলব্ধ। এই épisোডে, আমরা টেমপ্লেটিং-এর বিভিন্ন পদ্ধতি ইনভেস্টিগেট করব। আমরা স্ট্রিং কনক্যাটেনেশন থেকে শুরু করব, এবং আমাদের নিজস্ব লেক্সার লিখে শেষ করব যাতে আরও কমপ্লেক্স টেমপ্লেট সম্ভব হয়।

নেটিভ স্ট্রিং ইন্টারপোলেশন ব্যবহার করা

একটি মিনিমাল উদাহরণ দিয়ে শুরু করি। আমাদের অ্যাপ্লিকেশনের একটি ওয়েলকাম মেসেজ প্রয়োজন যেটি একটি প্রজেক্ট নাম ইনক্লুড করে। এটি করার কুইকেস্ট পথ হলো রুবির বিল্ট-ইন স্ট্রিং ইন্টারপোলেশন ফিচার ব্যবহার করা।

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#% উভয়ই ডেটার বিভিন্ন টাইপ হ্যান্ডেল করার স্পেশাল সিনট্যাক্স আ্যালোও করে। সবগুলো আমাদের পাস করা ডেটার সাথে কম্প্যাটিবল নয়। এই উদাহরণে, টেমপ্লেট এক্সপেক্ট করে একটি নম্বর ফরম্যাট করা কিন্তু একটি 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" বলে। ফরম্যাল ল্যাঙ্গুয়েজে—যেন প্রোগ্রামিং ল্যাঙ্গুয়েজ— সেগুলোকে টোকেনস বলা হয়। একটি লেক্সার কাজ করে টেমপ্লেট পড়ে এবং টেক্সটের স্ট্রিমকে প্রতিটি ক্যাটেগরির জন্য একটি সেট রেগুলার এক্সপ্রেশন দিয়ে ম্যাচ করে একটি নির্দিষ্ট অর্ডারে। প্রথম যে ম্যাচ করে সেটি টোকেনের ক্যাটেগরি ডিফাইন করে এবং রিলেভেন্ট ডেটা আট্যাচ করে। এই ছোট থিওরির সাথে, চলুন আমাদের টেমপ্লেট ল্যাঙ্গুয়েজের জন্য একটি লেক্সার ইমপ্লিমেন্ট করি। জিনিসপত্র একটু সহজ করতে, আমরা 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 কলগুলো এখন কন্ডিশনালের parçা, আমরা 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
এই চেঞ্জস inplace থাকলে, লেক্সার এখন আমাদের উদাহরণ সঠিকভাবে টোকেনাইজ করে।
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"] ]
এখন যেহেতু আমরা ফিনিশড, আমরা সাতটি ভিন্ন টাইপের টোকেন আইডেন্টিফাই করেছি:
TokenExample
OPEN_BLOCK{{#
OPEN_END_BLOCK{{/
OPEN_INVERSE{{else
OPEN_EXPRESSION{{
CONTENTএক্সপ্রেশনের বাইরে যেকোনো কিছু (নরমাল HTML বা Text)
CLOSE}}
IDENTIFIERআইডেন্টিফায়ারগুলো består করে ওয়ার্ড ক্যারেক্টার, নাম্বার, _, এবং -
পরবর্তী ধাপ হলো একটি পার্সার ইমপ্লিমেন্ট করা যেটি চেষ্টা করবে টোকেন স্ট্রিমের কাঠামো বুঝতে এবং সেটিকে একটি অ্যাবস্ট্র্যাক্ট সিনট্যাক্স ট্রিতে ট্রান্সলেট করা, কিন্তু সেটি অন্য কোনো সময়ের জন্য।

আগামী পথ

আমরা আমাদের নিজস্ব টেমপ্লেটিং ল্যাঙ্গুয়েজের দিকে যাত্রা শুরু করলাম স্ট্রিং ইন্টারপোলেশন ব্যবহার করে বেসিক টেমপ্লেটিং সিস্টেম ইমপ্লিমেন্ট করার বিভিন্ন পদ্ধতি দেখে। যখন আমরা প্রথম অপ্রোচের লিমিট হিট করলাম, আমরা একটি প্রপার টেমপ্লেটিং সিস্টেম ইমপ্লিমেন্ট করা শুরু করলাম। এখনও, আমরা একটি লেক্সার ইমপ্লিমেন্ট করেছি যা টেমপ্লেট অ্যানালাইস করে এবং বিভিন্ন টাইপের টোকেন বের করে। RubyMagic-এর একটি আগামী এডিশনে, আমরা যাত্রা চালিয়ে যাব পার্সার এবং ইন্টারপ্রেটার ইমপ্লিমেন্ট করে ইন্টারপোলেটেড স্ট্রিং জেনারেট করতে।

  1. অ্যান্ড্রয়েডে কাস্টম লিসেনার ইন্টারফেস তৈরি করা

  2. BASH-এ TXT টেমপ্লেট স্ক্রিপ্ট তৈরি করা

  3. রুবি মেথড ব্যাখ্যা: কাস্টম ফাংশন সহজে তৈরি করুন

  4. রুবি ২.৬-এ ৯টি গুরুত্বপূর্ণ উন্নতির অন্বেষণ