Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで独自のテンプレートレクサーを作成する

スキューバダイビングスイートを装着してステンシルを詰めてください。今日はテンプレートに飛び込みます!

Webページをレンダリングしたり、電子メールを生成したりするほとんどのソフトウェアは、テンプレートを使用して可変データをテキストドキュメントに埋め込みます。ドキュメントの主な構造は、多くの場合、データのプレースホルダーを含む静的テンプレートで設定されます。ユーザー名やWebページのコンテンツなどの可変データは、ページのレンダリング中にプレースホルダーを置き換えます。

テンプレートについて詳しく説明するために、多くのプログラミング言語で使用できるテンプレート言語である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#% 特別な構文でさまざまなタイプのデータを処理できるようにします。それらのすべてが、渡すデータと互換性があるわけではありません。この例では、テンプレートは数値をフォーマットすることを想定していますが、ハッシュが渡され、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}}

正規表現では、このユースケースをスムーズに処理できません。十分に努力すれば、おそらく一緒に何かをハックすることができますが、この時点で、適切なテンプレート言語を構築することをお勧めします。

テンプレート言語の構築

テンプレート言語の実装は、他のプログラミング言語の実装と似ています。スクリプト言語と同様に、テンプレート言語には、レクサー、パーサー、インタープリターの3つのコンポーネントが必要です。これらを1つずつ見ていきます。

レクサー

私たちが取り組む必要のある最初のタスクは、トークン化、または字句解析と呼ばれます。このプロセスは、自然言語で単語カテゴリを識別するのと非常によく似ています。

Ruby is a lovely languageのような例を見てください 。文は、異なるカテゴリの5つの単語で構成されています。それらがどのカテゴリであるかを識別するには、辞書を使用してすべての単語のカテゴリを検索します。これにより、次のようなリストが作成されます。名詞動詞記事形容詞名詞 。自然言語処理では、これらを「品詞」と呼びます。プログラミング言語などの形式言語では、トークンと呼ばれます。 。

レクサーは、テンプレートを読み取り、テキストのストリームを特定の順序で各カテゴリの正規表現のセットと照合することで機能します。最初に一致するものは、トークンのカテゴリを定義し、それに関連するデータを添付します。

この少しの理論が邪魔にならないように、テンプレート言語用のレクサーを実装しましょう。少し簡単にするために、StringScannerを使用します strscanを要求する Rubyの標準ライブラリから。 (ちなみに、StringScannerの優れたイントロがあります 以前のエディションの1つで。)最初のステップとして、すべてをCONTENTとして識別する最小バージョンを作成しましょう。 。

これを行うには、新しいStringScannerを作成します。 インスタンスを作成し、untilを使用してそのジョブを実行させます スキャナーが文字列の最後に到達したときにのみ停止するループ。

今のところ、すべての文字(.*)に一致させるだけです。 )複数行にまたがる(m 修飾子)と1つのCONTENTを返します そのすべてのトークン。トークンを配列として表し、トークン名を最初の要素、データを2番目の要素とします。非常に基本的なレクサーは次のようになります:

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}}で実行する場合 正確に1つの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は1つしか取得されません。 以前のようなトークン。開いている式まですべてを一致させるために、最後の式を変更する必要があります。これを行うには、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

レクサーを再度実行すると、4つのトークンが生成されます。

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

私たちのレクサーは、私たちが望む結果にかなり近いように見えます。ただし、name 通常のコンテンツではありません。それは識別子です!二重中括弧の間の文字列は、外側の文字列とは異なる方法で処理する必要があります。

ステートマシン

これを行うには、レクサーを2つの異なる状態を持つステートマシンに変換します。 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"]
]

終了したので、7つの異なるタイプのトークンを識別しました。

トークン
OPEN_BLOCK {{#
OPEN_END_BLOCK {{/
OPEN_INVERSE {{else
OPEN_EXPRESSION {{
CONTENT 式以外のもの(通常のHTMLまたはテキスト)
CLOSE }}
IDENTIFIER 識別子は、単語の文字、数字、_で構成されます 、および-

次のステップは、トークンストリームの構造を理解し、それを抽象構文ツリーに変換しようとするパーサーを実装することですが、それはまた別の機会です。

今後の道

文字列補間を使用して基本的なテンプレートシステムを実装するさまざまな方法を検討することから、独自のテンプレート言語への旅を始めました。最初のアプローチの限界に達したとき、適切なテンプレートシステムの実装を開始しました。

今のところ、テンプレートを分析し、さまざまなタイプのトークンを把握するレクサーを実装しました。 Ruby Magicの次のエディションでは、補間された文字列を生成するためのパーサーとインタープリターを実装することで、旅を続けます。


  1. Rubyの関数とメソッド:独自の関数を定義する方法

    Rubyメソッドとは何ですか? メソッドは、特定の目的のためにグループ化された1行または複数行のRubyコードです。 このグループ化されたコードには名前が付けられているため、コードを再度記述したり、コピーして貼り付けたりすることなく、いつでも使用できます。 メソッドの目的は次のとおりです : 情報を取得します。 オブジェクトを変更または作成します。 フィルターとフォーマットのデータ。 例1 : サイズ Arrayのメソッド オブジェクトは要素の数を示します(情報を取得します)。 例2 : pop メソッドは、配列から最後の要素を削除します(オブジェクトを変更します)。

  2. Ruby2.6の9つの新機能

    Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,