কম্পিউটার

রুবিতে আমাদের নিজস্ব টেমপ্লেট লেক্সার তৈরি করা

আপনার স্কুবা ডাইভিং স্যুট রাখুন এবং আপনার স্টেনসিলগুলি প্যাক করুন, আমরা আজ টেমপ্লেটগুলিতে ডুব দিচ্ছি!

বেশিরভাগ সফ্টওয়্যার যা ওয়েব পৃষ্ঠাগুলিকে রেন্ডার করে বা ইমেল তৈরি করে তারা টেক্সট নথিতে পরিবর্তনশীল ডেটা এম্বেড করার জন্য টেমপ্লেটিং ব্যবহার করে। নথির মূল কাঠামোটি প্রায়শই ডেটার জন্য স্থানধারক সহ একটি স্ট্যাটিক টেমপ্লেটে সেট আপ করা হয়। পরিবর্তনশীল ডেটা, যেমন ব্যবহারকারীর নাম বা ওয়েব পৃষ্ঠার বিষয়বস্তু, পৃষ্ঠাটি রেন্ডার করার সময় স্থানধারকগুলিকে প্রতিস্থাপন করে৷

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

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

একটি ন্যূনতম উদাহরণ দিয়ে শুরু করা যাক। আমাদের অ্যাপ্লিকেশনের একটি স্বাগত বার্তা প্রয়োজন যা একটি প্রকল্পের নাম অন্তর্ভুক্ত করতে হবে। এটি করার দ্রুততম উপায় হল রুবির অন্তর্নির্মিত স্ট্রিং ইন্টারপোলেশন বৈশিষ্ট্য ব্যবহার করা৷

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 শনাক্তকারীরা Word অক্ষর, সংখ্যা, _ নিয়ে গঠিত , এবং -

পরবর্তী পদক্ষেপটি হল একটি পার্সার প্রয়োগ করা যা টোকেন স্ট্রিমের গঠন খুঁজে বের করার চেষ্টা করে এবং এটিকে একটি বিমূর্ত সিনট্যাক্স ট্রিতে অনুবাদ করে, কিন্তু এটি অন্য সময়ের জন্য।

আগের রাস্তা

আমরা স্ট্রিং ইন্টারপোলেশন ব্যবহার করে একটি মৌলিক টেমপ্লেটিং সিস্টেম বাস্তবায়নের বিভিন্ন উপায় দেখে আমাদের নিজস্ব টেমপ্লেটিং ভাষার দিকে আমাদের যাত্রা শুরু করেছি। যখন আমরা প্রথম পদ্ধতির সীমা অতিক্রম করি, তখন আমরা একটি সঠিক টেমপ্লেটিং সিস্টেম বাস্তবায়ন শুরু করি৷

আপাতত, আমরা একটি লেক্সার প্রয়োগ করেছি যেটি টেমপ্লেট বিশ্লেষণ করে এবং বিভিন্ন ধরনের টোকেন বের করে। রুবি ম্যাজিকের একটি আসন্ন সংস্করণে, আমরা একটি পার্সার এবং সেইসাথে একটি ইন্টারপোলেটেড স্ট্রিং তৈরি করতে একটি দোভাষী প্রয়োগ করে যাত্রা চালিয়ে যাব৷


  1. অ্যান্ড্রয়েডে আমাদের নিজস্ব লিসেনার ইন্টারফেস কীভাবে তৈরি করবেন?

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

  3. রুবি ফাংশন এবং পদ্ধতি:আপনার নিজের সংজ্ঞায়িত কিভাবে

  4. রুবি 2.6-এ 9টি নতুন বৈশিষ্ট্য